AWS-Signature-V4

 view release on metacpan or  search on metacpan

README  view on Meta::CPAN

NAME

    AWS::Signature::V4 - User-Agent agnostic AWS Signatures V4 for
    credentials and X509

VERSION

    This document describes AWS::Signature::V4 version 0.001.

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};

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:

      * the traditional one, based on credentials (algorithm
      AWS4-HMAC-SHA256), with optional session token;

      * the one based on X.509 certificates, as used by IAM Roles Anywhere
      (algorithms AWS4-X509-RSA-SHA256 and AWS4-X509-ECDSA-SHA256). The
      signature is made with the private key that goes with the
      certificate, using CryptX (Crypt::PK::RSA and Crypt::PK::ECC) or a
      signing function of your own.

    Both header-based signing ("sign") and presigned URLs ("presign") are
    available, as well as chunked and streaming uploads to S3 (see "sign"
    and "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:

      * the url must be ASCII, with anything else already percent-encoded;

      * the body is a byte string, or a reference to one: encode text as
      the user agent will, wide characters are an error;

      * headers are a hash reference or an array reference of pairs, with
      names that are case insensitive.

    Defaults that depend on the service (e.g. S3 handles path encoding
    differently from all other services) are set from the service name, and
    can be overridden (see "new").

INTERFACE

 new

       my $s = AWS::Signature::V4->new(%args);

    Create a signer. service and region are mandatory (for the X.509
    variant used with IAM Roles Anywhere, the service is rolesanywhere) and
    can only hold letters, digits, ., _ and -; exactly one of credentials
    or x509 must be provided.

    The classes of this distribution are built with 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. $s->region).

    Options that are undef are treated as missing. This is important: if
    you want to pass a false value in an input that accepts a boolean
    value, use 0 for false.

    The constructor checks the options, including that certificates and
    keys can be read and loaded, and that 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.
    credentials and 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 ->credentials includes the
    secret access key. private_key and private_key_password are dropped
    from the copy of x509 as soon as the key is loaded, so ->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:
    AWS::Signature::V4::Credentials and AWS::Signature::V4::X509. They are
    documented for the record, but you do not need to know about them.

    credentials

      hash reference with access_key_id, secret_access_key and the optional
      session_token. Other keys are an error, so that a misspelled
      session_token is not silently ignored. An empty value is like a
      missing one: an error for the first two, no token for session_token
      (so that an empty AWS_SESSION_TOKEN can be passed as it is).

    x509

      hash reference with the following keys (others are an error):

      key_type

	RSA or ECDSA, depending on the key (mandatory). It determines the
	signing algorithm, see "algorithm";

      certificate, certificate_file

	the certificate, either as PEM or DER content, or as the path of a
	file holding it. One of them is needed, certificate wins if both
	are given. If the PEM holds several certificates (e.g. a
	fullchain.pem), only the first one is used and the others are
	ignored: put the intermediate certificates in chain or chain_files;

      chain, chain_files

	optional intermediate certificates. chain is either an array
	reference of certificates (PEM or DER content, possibly mixed) or a
	plain string, that then MUST be PEM. chain_files is either an array
	reference of file paths or a single path, each file holding PEM or
	DER content. 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 BEGIN
	CERTIFICATE/END CERTIFICATE blocks is ignored, so they can be
	separated by empty or whitespace-only lines, and CRLF line endings
	are fine;

      serial

	the serial number of the certificate, in decimal. It is taken from
	the certificate if not provided;

      private_key_file, private_key

	the private key, either as the path of a file or as its content, in
	PEM or DER format (as understood by CryptX). private_key wins if
	both are given;

      private_key_password

	only for encrypted keys. Like private_key, it is not kept once the
	key is loaded;

      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 first if your HSM or KMS gives it in base64 or hex: the

README  view on Meta::CPAN

      not a pipe or a socket) and binary, without layers like :encoding or
      :crlf that change the bytes read. Only one of them can be used;

    payload_hash, unsigned_payload

      skip the calculation and use the provided SHA-256 in hex, or
      UNSIGNED-PAYLOAD. They take precedence over body and body_fh.
      payload_hash can also be a STREAMING-* marker, anything else is an
      error. When the payload hash is not a SHA-256, the
      x-amz-content-sha256 header carries it, whatever the service;

    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. user-agent) or that are meant for a single hop (e.g.
      keep-alive). host and all the x-amz-* headers, including those that
      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;

    time

      the epoch to sign for, in seconds (a fractional part is dropped),
      defaults to now;

    streaming, decoded_content_length, checksum, trailers

      chunked and streaming uploads, see below.

    The returned hash reference contains:

    headers

      the complete set of headers to send, with lowercase names. Beyond
      those in input, they include host, x-amz-date, authorization and,
      depending on the case, x-amz-security-token, x-amz-x509,
      x-amz-x509-chain, x-amz-content-sha256;

    authorization

      the value of the Authorization header;

    signature, signed_headers, scope

      the signature in hex, the semicolon-separated list of signed headers,
      and the credential scope;

    canonical_request, string_to_sign

      the intermediate values of the algorithm, handy for debugging;

    chunker

      only when streaming: see below.

  Chunked and streaming uploads

    streaming enables the aws-chunked encoding used for uploads to S3,
    where the body is sent in chunks that are signed as they go. It can be
    1 or signed (each chunk is signed, credentials variant only) or
    unsigned (no chunk signatures, only the request headers are signed,
    also OK with X.509). The decoded_content_length, i.e. the size of the
    data, is mandatory. sign sets the payload hash to
    STREAMING-AWS4-HMAC-SHA256-PAYLOAD (or its variants below), adds
    x-amz-decoded-content-length and aws-chunked to Content-Encoding (after
    any other encoding, e.g. gzip,aws-chunked, as it is the one applied
    last: S3 takes that token off the end and stores what is left, here
    gzip), all signed; the Content-Length to provide is the size of the
    encoded body, see "encoded_length". S3 wants all chunks but the last
    one to be at least 8 KiB. body, body_fh, payload_hash and
    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 sign, which adds the
    x-amz-trailer header (signed like the others):

    checksum

      the name of an algorithm that the chunker computes while the chunks
      go through: crc32, crc32c, sha1 or sha256. The trailer is named after
      it (e.g. x-amz-checksum-crc32c);

    trailers

      array reference of names of trailers whose values you provide when
      calling "finish", so that any other algorithm can be used, e.g.
      x-amz-checksum-crc64nvme.

    Unsigned streaming needs at least one trailer. The payload hash becomes
    STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER or
    STREAMING-UNSIGNED-PAYLOAD-TRAILER, and with signed chunks the trailers
    get their own signature.

    S3 takes only one x-amz-checksum-* per request: use either checksum or
    a checksum in 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 }) },
       );
       my $ck = $r->{chunker};
       ...
       print {$socket} $ck->finish('x-amz-checksum-crc64nvme' => $base64);

README  view on Meta::CPAN

    data in order.

  chunk

       my $encoded = $chunker->chunk($data);

    Return the encoded version of a piece of data, to be sent as it is.
    $data is a byte string or a reference to one, it cannot be empty. It is
    an error to go past the decoded_content_length.

  finish

       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 decoded_content_length. The
    values of the trailers declared in 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: finish can be called again.

ERRORS

    Errors are reported by throwing Ouch exceptions (see $@->code,
    $@->message). The code is 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 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 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.

SECURITY CONSIDERATIONS

      * URLs. A URL must be ASCII, without control characters and without
      user information (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, .,
      _, ~ and -, or an IP address, with an optional numeric port.

      * Authorize on what is signed. For every service but S3 the path is
      normalized before signing, as AWS does: /public/../admin/delete is
      signed as /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 canonical_request), not at the raw URL, or it can be
      bypassed. For those same services, dot segments that are
      percent-encoded (e.g. %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 /public/%2e%2e/admin is signed as
      it is.

      * Do not log the results. headers and authorization can be used to
      replay the request for up to 15 minutes; a presigned URL is a bearer
      token until it expires; headers, canonical_request and presigned URLs
      include the session token, if there is one. Treat them as secrets.

      * Secrets in memory. The signer keeps the credentials (the secret
      access key included) as long as it lives, and ->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.

      * Error messages may include values that the caller provided, with
      non-printable characters escaped (e.g. \x{A}), so that they cannot
      forge lines in a log.

      * Dependencies are listed with their versions in cpanfile.snapshot;
      check them regularly against published advisories, e.g. with
      CPAN::Audit.

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 https://codeberg.org/polettix/AWS-Signature-V4.

AUTHOR

    Flavio Poletti <flavio@polettix.it>

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.

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

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    implied. See the License for the specific language governing
    permissions and limitations under the License.



( run in 0.888 second using v1.01-cache-2.11-cpan-85d3896f969 )