AWS-Signature-V4
view release on metacpan or search on metacpan
lib/AWS/Signature/V4.pod view on Meta::CPAN
=pod
=for vim
vim: tw=72 ts=3 sts=3 sw=3 et ai :
=encoding utf8
=head1 NAME
AWS::Signature::V4 - User-Agent agnostic AWS Signatures V4 for credentials and X509
=head1 VERSION
This document describes AWS::Signature::V4 version 0.001.
=head1 SYNOPSIS
use AWS::Signature::V4;
# traditional variant, based on credentials
my $s = AWS::Signature::V4->new(
service => 'iam', region => 'us-east-1',
credentials => {
access_key_id => $key_id,
secret_access_key => $secret,
session_token => $token, # optional
},
);
# certificate-based variant (IAM Roles Anywhere)
my $x = AWS::Signature::V4->new(
service => 'rolesanywhere', region => 'eu-west-1',
x509 => {
key_type => 'ECDSA', # or RSA
certificate_file => 'cert.pem',
private_key_file => 'key.pem',
},
);
# sign a request: nothing is sent, nothing depends on the user agent
my $r = $s->sign(
method => 'POST',
url => 'https://iam.amazonaws.com/?Action=ListUsers',
headers => { 'Content-Type' => 'application/json' },
body => $payload,
);
$ua_request->header($_ => $r->{headers}{$_}) for keys $r->{headers}->%*;
# presigned URL, i.e. signature in the query string; the signer must
# be for the service of the URL, S3 here
my $s3 = AWS::Signature::V4->new(
service => 's3', region => 'us-east-1',
credentials => {
access_key_id => $key_id,
secret_access_key => $secret,
},
);
my $p = $s3->presign(
url => 'https://bucket.s3.amazonaws.com/key',
expires => 3600,
);
my $url = $p->{url};
=head1 DESCRIPTION
This module implements the AWS Signature Version 4 algorithm without
being tied to any specific user agent: it does not send anything, it
just takes the pieces of a request (method, URL, headers, body) and
returns what has to be added to it, so that it can be used with whatever
HTTP client is at hand.
Two variants are supported:
=over
=item *
the traditional one, based on credentials (algorithm
C<AWS4-HMAC-SHA256>), with optional session token;
=item *
the one based on X.509 certificates, as used by IAM Roles Anywhere
(algorithms C<AWS4-X509-RSA-SHA256> and C<AWS4-X509-ECDSA-SHA256>). The
signature is made with the private key that goes with the certificate,
using L<CryptX> (L<Crypt::PK::RSA> and L<Crypt::PK::ECC>) or a signing
function of your own.
=back
Both header-based signing (L</sign>) and presigned URLs (L</presign>)
are available, as well as chunked and streaming uploads to S3 (see
L</sign> and L</encoded_length>) and hashing of large payloads without
loading them in memory.
Some conventions apply to the inputs, because what is signed has to be
exactly what goes on the wire and the module cannot guess an encoding:
=over
=item *
the C<url> must be ASCII, with anything else already percent-encoded;
=item *
the body is a I<byte string>, or a reference to one: encode text as the
user agent will, wide characters are an error;
=item *
headers are a hash reference or an array reference of pairs, with names
that are case insensitive.
=back
Defaults that depend on the service (e.g. S3 handles path encoding
differently from all other services) are set from the C<service> name,
and can be overridden (see L</new>).
=head1 INTERFACE
=head2 new
my $s = AWS::Signature::V4->new(%args);
Create a signer. C<service> and C<region> are mandatory (for the
X.509 variant used with IAM Roles Anywhere, the service is
C<rolesanywhere>) and can only hold letters, digits, C<.>, C<_> and
C<->; exactly one of C<credentials> or C<x509> must be provided.
The classes of this distribution are built with L<Moo>: the constructor
also accepts a hash reference, and every option below is available as a
read-only accessor of the same name (e.g. C<< $s->region >>).
Options that are C<undef> are treated as missing. B<This is important>:
if you want to pass a false value in an input that accepts a boolean value,
use C<0> for I<false>.
The constructor checks the options, including that certificates and keys
can be read and loaded, and that C<signer> is a code reference, so
problems are reported by it and not by the first signature; it does not
check that a certificate is otherwise valid, nor that a key matches it.
C<credentials> and C<x509> are copied (shallowly), so changing your own
hash afterwards has no effect, but the accessors give back the copies
held by the object: leave them alone. Mind that C<< ->credentials >>
includes the secret access key. C<private_key> and
C<private_key_password> are dropped from the copy of C<x509> as soon as
the key is loaded, so C<< ->x509 >> does not have them. Subclasses and
roles work as usual.
The two variants are implemented by two internal classes, one per
option, that this class uses on your behalf:
L<AWS::Signature::V4::Credentials> and L<AWS::Signature::V4::X509>. They
are documented for the record, but you do not need to know about them.
=over
=item C<credentials>
hash reference with C<access_key_id>, C<secret_access_key> and the
optional C<session_token>. Other keys are an error, so that a misspelled
C<session_token> is not silently ignored. An empty value is like a
missing one: an error for the first two, no token for C<session_token>
(so that an empty C<AWS_SESSION_TOKEN> can be passed as it is).
=item C<x509>
hash reference with the following keys (others are an error):
=over
=item C<key_type>
C<RSA> or C<ECDSA>, depending on the key (mandatory). It determines the
signing algorithm, see L</algorithm>;
=item C<certificate>, C<certificate_file>
the certificate, either as PEM or DER content, or as the path of a file
holding it. One of them is needed, C<certificate> wins if both are given.
If the PEM holds several certificates (e.g. a F<fullchain.pem>), only the
first one is used and the others are ignored: put the intermediate
certificates in C<chain> or C<chain_files>;
=item C<chain>, C<chain_files>
optional intermediate certificates. C<chain> is either an array
reference of certificates (PEM or DER content, possibly mixed) or a plain
string, that then MUST be PEM. C<chain_files> is either an array
reference of file paths or a single path, each file holding PEM or DER
content. C<chain> wins if both are given.
A PEM item, whether from a string or a file, can be a bundle of several
certificates: whatever sits between the C<BEGIN CERTIFICATE>/C<END
CERTIFICATE> blocks is ignored, so they can be separated by empty or
whitespace-only lines, and CRLF line endings are fine;
=item C<serial>
the serial number of the certificate, in decimal. It is taken from the
certificate if not provided;
=item C<private_key_file>, C<private_key>
the private key, either as the path of a file or as its content, in PEM
or DER format (as understood by L<CryptX>). C<private_key> wins if both
are given;
=item C<private_key_password>
only for encrypted keys. Like C<private_key>, it is not kept once the key
is loaded;
=item C<signer>
alternative to the keys: a function that gets the bytes to sign and
returns the signature of their SHA-256, as raw bytes: PKCS#1 v1.5 for
RSA and DER for ECDSA (e.g. to delegate to an HSM or a KMS). Decode it
lib/AWS/Signature/V4.pod view on Meta::CPAN
C<UNSIGNED-PAYLOAD>. They take precedence over C<body> and C<body_fh>.
C<payload_hash> can also be a C<STREAMING-*> marker, anything else is an
error. When the payload hash is not a SHA-256, the
C<x-amz-content-sha256> header carries it, whatever the service;
=item C<signed_headers>
array reference of the names of the headers to sign. By default all the
headers are signed except a few that are commonly changed on the way
(e.g. C<user-agent>) or that are meant for a single hop (e.g.
C<keep-alive>). C<host> and all the C<x-amz-*> headers, including those
that C<sign> adds, are always signed, even if not in the list: AWS wants
them signed, and it binds the signature to the host, the date and the
session token. It is an error to name a missing header;
=item C<time>
the epoch to sign for, in seconds (a fractional part is dropped),
defaults to now;
=item C<streaming>, C<decoded_content_length>, C<checksum>, C<trailers>
chunked and streaming uploads, see below.
=back
The returned hash reference contains:
=over
=item C<headers>
the complete set of headers to send, with lowercase names. Beyond those
in input, they include C<host>, C<x-amz-date>, C<authorization> and,
depending on the case, C<x-amz-security-token>, C<x-amz-x509>,
C<x-amz-x509-chain>, C<x-amz-content-sha256>;
=item C<authorization>
the value of the C<Authorization> header;
=item C<signature>, C<signed_headers>, C<scope>
the signature in hex, the semicolon-separated list of signed headers,
and the credential scope;
=item C<canonical_request>, C<string_to_sign>
the intermediate values of the algorithm, handy for debugging;
=item C<chunker>
only when streaming: see below.
=back
=head3 Chunked and streaming uploads
C<streaming> enables the C<aws-chunked> encoding used for uploads to S3,
where the body is sent in chunks that are signed as they go. It can be
C<1> or C<signed> (each chunk is signed, credentials variant only) or
C<unsigned> (no chunk signatures, only the request headers are signed,
also OK with X.509). The C<decoded_content_length>, i.e. the size of
the data, is mandatory. C<sign> sets the payload hash to
C<STREAMING-AWS4-HMAC-SHA256-PAYLOAD> (or its variants below), adds
C<x-amz-decoded-content-length> and C<aws-chunked> to C<Content-Encoding>
(after any other encoding, e.g. C<gzip,aws-chunked>, as it is the one
applied last: S3 takes that token off the end and stores what is left,
here C<gzip>), all signed; the C<Content-Length> to provide is the size
of the encoded body, see L</encoded_length>. S3 wants all chunks but the last
one to be at least 8 KiB. C<body>, C<body_fh>, C<payload_hash> and
C<unsigned_payload> do not apply.
my $length = AWS::Signature::V4->encoded_length($decoded_length, $chunk_size);
my $r = $s->sign(
method => 'PUT', url => $url,
headers => { 'Content-Length' => $length },
streaming => 1, decoded_content_length => $decoded_length,
);
# send $r->{headers}, then the body, made of encoded chunks:
my $ck = $r->{chunker};
print {$socket} $ck->chunk($_) for @chunks;
print {$socket} $ck->finish;
Trailers, i.e. headers sent after the data, e.g. for a checksum that is
only known at the end, are declared with C<sign>, which adds the
C<x-amz-trailer> header (signed like the others):
=over
=item C<checksum>
the name of an algorithm that the chunker computes while the chunks go
through: C<crc32>, C<crc32c>, C<sha1> or C<sha256>. The trailer is
named after it (e.g. C<x-amz-checksum-crc32c>);
=item C<trailers>
array reference of names of trailers whose values you provide when
calling L</finish>, so that any other algorithm can be used, e.g.
C<x-amz-checksum-crc64nvme>.
=back
Unsigned streaming needs at least one trailer. The payload hash becomes
C<STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER> or
C<STREAMING-UNSIGNED-PAYLOAD-TRAILER>, and with signed chunks the trailers
get their own signature.
S3 takes only one C<x-amz-checksum-*> per request: use either
C<checksum> or a checksum in C<trailers>, not both. For example, with a
CRC-64 computed by you:
my $r = $s->sign(
method => 'PUT', url => $url, streaming => 'signed',
decoded_content_length => $decoded_length,
trailers => ['x-amz-checksum-crc64nvme'],
headers => { 'Content-Length' => AWS::Signature::V4->encoded_length(
$decoded_length, $chunk_size,
trailers => { 'x-amz-checksum-crc64nvme' => 12 }) },
);
lib/AWS/Signature/V4.pod view on Meta::CPAN
my $encoded = $chunker->finish(%trailer_values);
Return the final, empty chunk, followed by the trailers if any. It is an
error if the amount of data is not C<decoded_content_length>. The values
of the trailers declared in C<trailers> are passed by name, in any case;
it is an error to omit one, to pass one twice, to pass one that was not
declared or that is computed by the chunker, or to pass a value that is
not a byte string or has a CR, LF or NUL in it. After an error, the
chunker is as it was: C<finish> can be called again.
=head1 ERRORS
Errors are reported by throwing L<Ouch> exceptions (see C<< $@->code >>,
C<< $@->message >>). The code is C<400> when the problem is in what the
caller provided, e.g. a missing option, an invalid URL, a certificate that
cannot be read, a chunk that goes past the declared size; it is C<500>
when the fault is in the module itself, i.e. something that should not
happen whatever the input. The exception is reported at the line of the
caller, and its C<trace> does not include the arguments of the calls, as
they may hold secrets like passwords. Moo's own complaints, e.g. about the
arguments of the constructors that are not meant to be called directly,
are not Ouch exceptions.
=head1 SECURITY CONSIDERATIONS
=over
=item *
B<URLs.> A URL must be ASCII, without control characters and without user
information (C<user@host>): AWS endpoints never have it, and user agents
do not all agree on which host such a URL points to. For the same reason
the host can only be a name made of letters, digits, C<.>, C<_>, C<~>
and C<->, or an IP address, with an optional numeric port.
=item *
B<Authorize on what is signed.> For every service but S3 the path is
normalized before signing, as AWS does: C</public/../admin/delete> is
signed as C</admin/delete>, and that is what AWS executes. If your
application decides whether a caller may have a request signed by
looking at the URL, it must look at the normalized path (see the second
line of C<canonical_request>), not at the raw URL, or it can be bypassed.
For those same services, dot segments that are percent-encoded (e.g.
C<%2e%2e>) are an error, as it is not certain how AWS reads them; S3 does
not normalize at all, so there they are ordinary characters of the key and
C</public/%2e%2e/admin> is signed as it is.
=item *
B<Do not log the results.> C<headers> and C<authorization> can be used to
replay the request for up to 15 minutes; a presigned URL is a bearer token
until it expires; C<headers>, C<canonical_request> and presigned URLs
include the session token, if there is one. Treat them as secrets.
=item *
B<Secrets in memory.> The signer keeps the credentials (the secret access
key included) as long as it lives, and C<< ->credentials >> gives them
back. The text of an X.509 private key and its password are dropped once
the key is loaded. The key derived for signed chunks, which could sign
any request for the same service, region and day, is kept by the chunker
in a closure: no accessor gives back its bytes and dumping the chunker
does not show them, but whoever holds the chunker can still sign with it
through its internals. Do not hand the chunker, let alone the signer, to
code that you would not trust with the credentials.
=item *
B<Error messages> may include values that the caller provided, with
non-printable characters escaped (e.g. C<\x{A}>), so that they cannot
forge lines in a log.
=item *
B<Dependencies> are listed with their versions in F<cpanfile.snapshot>;
check them regularly against published advisories, e.g. with
L<CPAN::Audit>.
=back
=head1 BUGS AND LIMITATIONS
Minimum perl version 5.24.
Only Signature Version 4 is supported, not Version 4A (asymmetric,
multi-region). Event stream signing is not supported either.
Signing with the X.509 variant, including presigned URLs and the
transport of the certificate in the query string, follows the algorithm
as documented for IAM Roles Anywhere but has only been tested with local
signatures.
The signature of trailers in streaming uploads has not been checked
against AWS.
Please report any bug or feature request through the repository of the
project, available at L<https://codeberg.org/polettix/AWS-Signature-V4>.
=head1 AUTHOR
Flavio Poletti <flavio@polettix.it>
=head1 AI ASSISTANCE
This module was developed with the assistance of artificial intelligence
(Anthropic Claude 5). The AI was used to generate the code based on the
specifications from Amazon and directions from the author to organize
the code and use relevant base modules.
All AI-generated code has been manually audited, refactored, and
verified by the maintainer to ensure compliance with Perl best practices
and security standards.
=head1 COPYRIGHT AND LICENSE
Copyright 2026 by Flavio Poletti <flavio@polettix.it>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
( run in 0.723 second using v1.01-cache-2.11-cpan-85d3896f969 )