AWS-Signature-V4

 view release on metacpan or  search on metacpan

LICENSE  view on Meta::CPAN

      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a

MANIFEST  view on Meta::CPAN

lib/AWS/Signature/V4/Credentials.pod
lib/AWS/Signature/V4/Error.pm
lib/AWS/Signature/V4/Error.pod
lib/AWS/Signature/V4/X509.pm
lib/AWS/Signature/V4/X509.pod
script/aws-iam-ra
t/author-pod-syntax.t
t/basic.t
t/canonical.t
t/chain.t
t/error.t
t/errors.t
t/presign.t
t/security.t
t/streaming.t
t/trailer.t
t/variants.t

README  view on Meta::CPAN

    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

README  view on Meta::CPAN

    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

README  view on Meta::CPAN

      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
	returned bytes are put in the request as they are, hex-encoded, and
	AWS would just refuse a signature in the wrong form. It takes
	precedence over the keys. If it returns undef, an empty string, a
	reference or a string with wide characters, signing fails with an
	Ouch exception with code 400; if it dies, the error goes through as
	it is.

    double_encode, normalize_path, payload_header

      boolean overrides for what is decided from the service: encode the
      path twice, remove empty, . and .. segments from it, and always add
      the x-amz-content-sha256 header. They default to true, true, false
      for every service but S3, where they are false, false, true. S3 goes
      by several names: s3, s3-object-lambda, s3-outposts and s3express.

      Again, setting an undef value for these three arguments does NOT mean
      false but default instead (i.e. false, false, and true).

      payload_header also tells "presign" what the service takes for
      granted in a presigned URL: UNSIGNED-PAYLOAD if true, as S3 does, the

README  view on Meta::CPAN


    The name of the signing algorithm in use, e.g. AWS4-HMAC-SHA256.

 sign

       my $r = $s->sign(%args);
       my $r = $s->sign(\%args);

    Sign a request. The arguments are name/value pairs or a hash reference;
    an unknown name, or one that does not apply (e.g. expires, which is for
    "presign"), is an error, and an undef value is the same as a missing
    argument. Named arguments:

    method, url

      the HTTP method and the full URL (mandatory). The method must be an
      HTTP token, and it is uppercased. The host, lowercased and without
      default ports, provides the host header unless it is given; user
      information (user@host) is an error, it is not stripped. The URL can
      also be just a path, starting with /, if a Host header is given. See
      "DESCRIPTION" and "SECURITY CONSIDERATIONS" for what the URL can
      hold; in particular, the query cannot hold +, which some read as a
      space and others as a plus sign: write %20 or %2B;

    headers

      the headers of the request, as a hash reference or an array reference
      of pairs. A value is a byte string, or an array reference of them,
      and repeated names are joined with a comma; undef values are skipped.
      Names must be HTTP tokens (RFC 9110: letters, digits and
      !#$%&'*+-.^_`|~) and values cannot hold CR, LF or NUL characters:
      anything else is an error, as it could inject headers or change the
      meaning of the canonical request. The same check applies to the
      values that sign adds, e.g. the session token, and to the ones that
      "presign" puts in the query string instead.

      An X-Amz-Content-Sha256 header is taken as the payload hash; if the
      payload hash also comes from the arguments below, the two must agree;

    body, body_fh

      the payload, as a byte string, a reference to one (which avoids

README  view on Meta::CPAN

      hashed from its current position to its end, without loading it in
      memory, and put back where it was: so it must be seekable (a file,
      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.

README  view on Meta::CPAN


    Only host and the x-amz-* headers are signed by default, other headers
    must be named in signed_headers (and provided). The session token and,
    for the X.509 variant, the certificate and chain travel as query
    parameters. The payload is UNSIGNED-PAYLOAD for S3 and the hash of the
    empty body for other services, unless body, body_fh, payload_hash,
    unsigned_payload or an X-Amz-Content-Sha256 header say otherwise. The
    service cannot know about a payload hash that it does not take for
    granted (see payload_header in "new"): in that case it goes in the
    x-amz-content-sha256 header, signed, which the client must send.
    Streaming does not apply, and its arguments are an error.

    The URL must not already carry any of the parameters that presign adds
    (X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires,
    X-Amz-SignedHeaders, X-Amz-Signature, X-Amz-Security-Token, X-Amz-X509,
    X-Amz-X509-Chain, in any case): it is an error, so that the URL cannot
    end up with two values for the same parameter.

    The returned hash reference has the url to hand out, with any character
    that is not allowed in a URL path (e.g. a space) percent-encoded, the
    headers that the client must send along (just host by default), and
    signature, signed_headers, scope, canonical_request and string_to_sign
    like in "sign".

 encoded_length

       my $length = AWS::Signature::V4->encoded_length($decoded, $chunk_size, %opts);

    Class method that computes the size of an aws-chunked body made of
    $decoded bytes of data cut in chunks of $chunk_size bytes (except for
    the last one, that can be shorter), which is what to put in the
    Content-Length header. The sizes must be integers, and unknown options
    are an error. The options mirror the choices made for "sign":

    signed

      true (1, default) if the chunks are signed, false (0 or the empty
      string) otherwise; signed and unsigned are also accepted, as for
      streaming in "sign";

    checksum

      the name of the built-in checksum, if any;

    trailers

      hash reference from the name of each trailer that will be given to
      "finish" to the length of its value (e.g. 12 for a base64-encoded
      CRC-64). As in "sign", the trailer of checksum must not be listed
      here as well: declaring the same name twice is an error.

 The chunker

    When streaming, "sign" returns a chunker that wraps the data into
    chunks, signing them if needed. It keeps track of the amount of data
    and of the signatures, so use it for one body only, and feed it the
    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

README  view on Meta::CPAN

      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

TODO.md  view on Meta::CPAN

Chunks 1 to 3 cover all the code: stopping after them already gives a
baseline for everything that runs.

- [x] **1. Signing core** (level `max`, ~890 lines):
      `lib/AWS/Signature/V4.pm` with `t/canonical.t`, `t/basic.t`,
      `t/presign.t`. Canonical request, encoding, presign,
      `encoded_length`.
- [x] **2. Streaming** (level `high`, ~440 lines):
      `lib/AWS/Signature/V4/Chunker.pm`, `Checksum.pm` with
      `t/streaming.t`, `t/trailer.t`.
- [x] **3. Variants and errors** (level `high`, ~760 lines):
      `lib/AWS/Signature/V4/X509.pm`, `Credentials.pm`, `Error.pm` with
      `t/chain.t`, `t/variants.t`, `t/error.t`, `t/errors.t`,
      `t/security.t`. DER parsing, key loading.
- [x] **4. Documentation** (level `medium`, ~1140 lines): all the `.pod`
      files, checked against the code: do they promise anything that the
      code does not do?
- [x] **5. Examples** (level `medium` or `low`, ~670 lines): `eg/*.pl`,
      `eg/README.md`. Lowest priority, can be skipped.

## Fixing the review findings

The findings are in `tmp/review-1-signing-core.md` to
`tmp/review-5-examples.md`. Fix them one file at a time, in the same order
as the review: later areas depend on earlier ones, and a fix in the code
often needs matching changes in the POD, the tests and `eg/`.

Model: Opus 5 at high effort (or xhigh) by default. Switch with `/model`
before a chunk when useful:

- Sonnet 5 is enough for chunks 4 and 5 (wording, error checks,
  comments), at a lower cost;
- Fable 5.1 (twice the cost of Opus 5) only if a fix in chunk 1 turns out
  to be hard and Opus's first attempt does not hold up;
- not Haiku: too easy to miss something in signing code.

For each chunk:

- start a new session (or `/clear`) first;
- ask to fix the findings in `tmp/review-<n>-<name>.md`, updating the
  POD, tests and examples affected by each fix;
- run the test suite and make sure it passes. The dependencies are
  installed in `local/` (by `carton`), so they have to be added to the
  include path: `prove -l -Ilocal/lib/perl5 t/`, or `carton exec prove -l
  t/`. Plain `prove -l t/` dies on `Can't locate Moo.pm`;
- make one commit per chunk.

- [x] **1. Signing core** (Opus 5, Fable 5.1 if needed):
      `tmp/review-1-signing-core.md`.
- [x] **2. Streaming** (Opus 5): `tmp/review-2-streaming.md`.
- [x] **3. Variants and errors** (Opus 5):
      `tmp/review-3-variants-errors.md`.
- [x] **4. Documentation** (Sonnet 5 or Opus 5):
      `tmp/review-4-documentation.md`.
- [x] **5. Examples** (Sonnet 5 or Opus 5): `tmp/review-5-examples.md`.
      Start with the medium finding in `eg/05-s3-chunked-upload.pl`.
- [x] **6. Final check** (Opus 5): run `/code-review high` on the whole
      diff of the fixes (from `d7b121f`, the last commit before them) to
      catch regressions the fixes introduced.
      `tmp/review-6-final.md`, fixed in `ed8fd99`.

## Before release

eg/07-rolesanywhere-x509.pl  view on Meta::CPAN

);
# beside Authorization, the headers now include X-Amz-X509 (the
# certificate) and, if there is one, X-Amz-X509-Chain

# HTTP::Tiny adds the Host header itself, from the URL, and refuses to be
# given one: it is signed all the same, and it will have the same value
my %headers = $r->{headers}->%*;
delete $headers{host};

if ($ENV{DRY_RUN}) {
   # on the standard error like everything that is not a session: a dry
   # run obtains none, so "> session.json" must come out empty rather
   # than hold a dump of the request that was never sent
   say {*STDERR} "POST $url";
   for my $name (sort keys $r->{headers}->%*) {
      my $value = $r->{headers}{$name};
      $value = substr($value, 0, 40) . '...' if length($value) > 60;
      say {*STDERR} "$name: $value";
   }
   say {*STDERR} "\n$body";
   exit 0;
}
my $response = HTTP::Tiny->new(verify_SSL => 1)->request(POST => $url,
   {headers => \%headers, content => $body});
say {*STDERR} "$response->{status} $response->{reason}";
# only a session reaches the standard output, so that "> session.json" or
# a pipe into jq gets JSON and nothing else. An error body is often not
# JSON at all: when the request never got to AWS the status is 599 and
# that field holds the reason as plain text ("Could not connect to ...").
# Saved in the place of a session, it would be a failure kept as if it
# were credentials, and read back as such much later
if (!$response->{success}) {
   say {*STDERR} $response->{content};    # this says what went wrong
   die "the request failed\n";
}
say {*STDOUT} $response->{content};

eg/10-s3-content-encoding-probe.pl  view on Meta::CPAN

sub without_host {
   my ($r) = @_;
   my %headers = $r->{headers}->%*;
   delete $headers{host};
   return \%headers;
}

# S3 reports failures as XML; the first two fields are the useful ones.
# HTTP::Tiny never reached it when the status is 599, and then the reason
# (no route, TLS refused, name not resolved) is in the content instead
sub s3_error {
   my ($response) = @_;
   my $content = $response->{content} // '';
   my ($code) = $content =~ m{<Code>([^<]*)</Code>};
   my ($msg)  = $content =~ m{<Message>([^<]*)</Message>};
   return one_line(defined $msg && length $msg ? "$code: $msg" : $code) if defined $code;
   return '' unless ($response->{status} // 0) == 599;
   return one_line($content);
}

# the S3 error code on its own (NoSuchBucket, AccessDenied, ...). Its
# presence is what tells a refusal by S3 apart from a failure that never
# reached it, which is the difference between an answer and a bad run
sub s3_code {
   my ($response) = @_;
   my ($code) = ($response->{content} // '') =~ m{<Code>([^<]*)</Code>};
   return undef unless defined $code && length $code;
   return one_line($code);
}

# the status line comes from the other end as well: HTTP::Tiny takes the

eg/10-s3-content-encoding-probe.pl  view on Meta::CPAN

   my ($text) = @_;
   return '' unless defined $text && length $text;
   $text =~ s{\s+}{ }g;
   $text =~ s{[^\x20-\x7E]}{.}g;
   $text =~ s{\A\s+}{};
   $text =~ s{\s+\z}{};
   $text = substr($text, 0, 197) . '...' if length $text > 200;
   return redact($text);
}

# both kinds of error quote the URL or the key back at us, and the report
# is meant to be pasted: keep the bucket and the key out of it
sub redact {
   my ($text) = @_;
   return $text unless defined $text && length $text;
   $text =~ s{\Q$bucket\E}{BUCKET}g;
   $text =~ s{\Q$prefix\E}{KEY-PREFIX/}g;
   return $text;
}

# HTTP::Tiny lowercases header names and gives an array reference when a

eg/10-s3-content-encoding-probe.pl  view on Meta::CPAN

   if ($ENV{DRY_RUN}) {
      $c->{dry} = sprintf 'body %d bytes, announced %d', length $body, $c->{announced};
      next;
   }

   my $put = $http->request(PUT => $url,
      {headers => without_host($r), content => $body});
   $c->{put}    = status_of($put);
   $c->{status} = $put->{status} // 0;
   if (!$put->{success}) {
      $c->{put_error} = s3_error($put);
      $c->{s3_code}   = s3_code($put);
      next;
   }
   $c->{ok} = 1;

   my $g = $signer->sign(method => 'GET', url => $url);
   my $get = $http->get($url, {headers => without_host($g)});
   $c->{get} = status_of($get);
   if ($get->{success}) {
      $c->{stored} = header_of($get, 'content-encoding');
      my $got = $get->{content} // '';
      $c->{body} =
           $got eq $gz    ? 'identical'
         : $got eq $plain ? 'DECOMPRESSED on the way (S3 or the client unzipped it)'
         : sprintf('DIFFERENT (%d bytes back, %d sent)', length $got, $size);
   }
   else { $c->{get_error} = s3_error($get) }

   next if $ENV{KEEP};
   my $d = $signer->sign(method => 'DELETE', url => $url);
   my $del = $http->request(DELETE => $url, {headers => without_host($d)});
   $c->{leftover} = $c->{key} unless $del->{success};
}

# --- the report, safe to paste anywhere ------------------------------------
my @out = (
   '--- aws-chunked ordering probe ------------------------',
   "module:   AWS::Signature::V4 " . (AWS::Signature::V4->VERSION // '(unknown)'),
   "region:   $region          (bucket and key not shown)",
   sprintf('payload:  %d bytes plain -> %d bytes gzip, %d data chunk%s',
      length $plain, $size, $chunks, $chunks == 1 ? '' : 's'),
   '',
);
for my $c (@cases) {
   push @out, sprintf('%-10s sent   Content-Encoding: %s', $c->{name}, $c->{sent});
   push @out, "           dry    $c->{dry}" if defined $c->{dry};
   push @out, "           PUT    $c->{put}" if defined $c->{put};
   push @out, "           error  $c->{put_error}"
      if defined $c->{put_error} && length $c->{put_error};
   push @out, "           GET    $c->{get}" . (defined $c->{stored}
      ? "   stored Content-Encoding: $c->{stored}" : '') if defined $c->{get};
   push @out, "           error  $c->{get_error}"
      if defined $c->{get_error} && length $c->{get_error};
   push @out, "           body   $c->{body}" if defined $c->{body};
   push @out, '';
}

my ($control, $gzip_case) = @cases;

# a 200 is not the answer on its own. The question is what S3 made of the
# list, and the run already went and collected that: the Content-Encoding
# it kept, and whether the bytes came back as they were sent
my $confirmed = $gzip_case->{ok}

eg/10-s3-content-encoding-probe.pl  view on Meta::CPAN

   push @out, '          the problem. Swap the last line of the $streaming';
   push @out, '          branch in lib/AWS/Signature/V4.pm to';
   push @out, q{             join ',', 'aws-chunked', @encodings;};
   push @out, '          update V4.pod and t/streaming.t to match, and run';
   push @out, '          this program again to confirm the other order.';
   $status = 1;
}
else {
   push @out, 'verdict:  INCONCLUSIVE. The gzip upload failed, but not with a';
   push @out, '          refusal from S3, so it says nothing about the order:';
   push @out, '          see the error line above, then run it again.';
   $status = 1;
}
# the keys themselves go on stderr, outside the block that gets pasted
my @leftover = grep { defined } map { $_->{leftover} } @cases;
if (@leftover) {
   push @out, '', sprintf 'note:     %d object%s left behind, delete %s by hand',
      scalar @leftover, @leftover == 1 ? '' : 's', @leftover == 1 ? 'it' : 'them';
   warn "could not delete: $_\n" for @leftover;
}
push @out, '-------------------------------------------------------';
say for @out;
exit $status;

eg/README.md  view on Meta::CPAN

```shell
CERT_FILE=cert.pem KEY_FILE=key.pem \
   TRUST_ANCHOR_ARN=arn:aws:rolesanywhere:... PROFILE_ARN=arn:aws:rolesanywhere:... \
   ROLE_ARN=arn:aws:iam::... ./eg/07-rolesanywhere-x509.pl

DRY_RUN=1 KEY_TYPE=ECDSA CERT_FILE=cert.pem KEY_FILE=key.enc.pem \
   KEY_PASSWORD=secret CHAIN_FILE=ca-bundle.pem ./eg/07-rolesanywhere-x509.pl
```

The session JSON is the only thing on the standard output — the
algorithm, the status line, any error body and the `DRY_RUN=1` dump of
the request all go to the standard error — so the program can be used as
`> session.json` or piped into `jq`. A dry run obtains no session and so
writes nothing there either. A failed request prints nothing at all on
the standard output, rather than
an error body where a session was expected: `HTTP::Tiny` reports a
request that never reached AWS as status 599 with the reason, as plain
text, in the body, and that saved under the name of a session would be a
failure kept as if it were credentials. The exit status is non-zero
either way. Mind that what does get saved holds temporary credentials,
so the file deserves the same care as a private key.

`AWS_REGION` is checked here too, for the same reason as in 02 to 05 and
10: it lands in the host name, and a `/` would send the certificate and
its signature somewhere else, with whatever answered read back as a
session.

eg/README.md  view on Meta::CPAN

AWS_REGION=eu-west-1 ./eg/10-s3-content-encoding-probe.pl my-bucket scratch/probe-
KEEP=1 ./eg/10-s3-content-encoding-probe.pl my-bucket
DRY_RUN=1 ./eg/10-s3-content-encoding-probe.pl my-bucket
```

A verdict is only reached when the run earns it. "Accepted" needs more
than a 200: S3 must also have stored the object as `Content-Encoding:
gzip`, having taken the `aws-chunked` token off, and have given the bytes
back unchanged. "Rejected", which tells you to change the order in the
signing code, needs S3 itself to have turned the upload down — a 4xx
carrying an S3 error code, and not one of the 401 and 403 that talk
about the credentials rather than the header. Everything else, a 503, a
dropped connection, a key that expired halfway through, is reported as
inconclusive, because the control having gone through says nothing about
the upload after it. It exits non-zero when the probe reaches no verdict,
or when the order is refused, and then says which line to change. The
report it prints is meant
to be pasted into a bug report or a chat: it is built from a fixed list of
fields, so it holds no credentials, no `Authorization` header, no session
token, and neither the bucket name nor the key.

lib/AWS/Signature/V4.pm  view on Meta::CPAN

         Proxy-Connection
         TE
         Trailer
         Transfer-Encoding
         Upgrade
         User-Agent
         X-Amzn-Trace-Id
      >
   },

   # the arguments of some methods; anything else is an error
   ALLOWED_ARGS => +{
      encoded_length => +{
         map { $_ => 1 } qw<
            checksum
            signed
            trailers
         >
      },

      sign => +{

lib/AWS/Signature/V4.pm  view on Meta::CPAN

};

sub _copy ($value) { ref $value eq 'HASH' ? {$value->%*} : $value }

sub _hash_or_undef ($name, $value) {
   fail 400, qq{"$name" must be a hash reference}
      unless !defined $value || ref $value eq 'HASH';
}

# arguments of sign/presign as a hash: name => value pairs or a hash
# reference; undefined values are like missing ones, unknown names an error
sub _args ($what, @args) {
   @args = $args[0]->%* if @args == 1 && ref $args[0] eq 'HASH';
   fail 400, "$what takes name => value pairs or a hash reference" if @args % 2;
   my %args = @args;
   defined $args{$_} or delete $args{$_} for keys %args;

   # go looking for argument names that are outside the allowed ones
   my $allowed = ALLOWED_ARGS->{$what}
      or die "ALLOWED_ARGS does not support <$what>";
   if (my @l = grep { ! $allowed->{$_} } keys(%args)) {

lib/AWS/Signature/V4.pod  view on Meta::CPAN


=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

lib/AWS/Signature/V4.pod  view on Meta::CPAN

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>

lib/AWS/Signature/V4.pod  view on Meta::CPAN

=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
first if your HSM or KMS gives it in base64 or hex: the returned bytes are
put in the request as they are, hex-encoded, and AWS would just refuse a
signature in the wrong form. It takes precedence over
the keys. If it returns C<undef>, an empty string, a reference or a string
with wide characters, signing fails with an L<Ouch> exception with code
C<400>; if it dies, the error goes through as it is.

=back

=item C<double_encode>, C<normalize_path>, C<payload_header>

boolean overrides for what is decided from the service: encode the
path twice, remove empty, C<.> and C<..> segments from it, and always
add the C<x-amz-content-sha256> header. They default to true, true, false
for every service but S3, where they are false, false, true. S3 goes by
several names: C<s3>, C<s3-object-lambda>, C<s3-outposts> and
C<s3express>.

Again, setting an C<undef> value for these three arguments does I<NOT>
mean I<false> but I<default> instead (i.e. false, false, and true).

C<payload_header> also tells L</presign> what the service takes for

lib/AWS/Signature/V4.pod  view on Meta::CPAN


The name of the signing algorithm in use, e.g. C<AWS4-HMAC-SHA256>.

=head2 sign

   my $r = $s->sign(%args);
   my $r = $s->sign(\%args);

Sign a request. The arguments are name/value pairs or a hash reference;
an unknown name, or one that does not apply (e.g. C<expires>, which is
for L</presign>), is an error, and an C<undef> value is the same as a
missing argument. Named arguments:

=over

=item C<method>, C<url>

the HTTP method and the full URL (mandatory). The method must be an HTTP
token, and it is uppercased. The host, lowercased and without default
ports, provides the C<host> header unless it is given; user information
(C<user@host>) is an error, it is not stripped.
The URL can also be just a path, starting with C</>, if a C<Host> header
is given. See L</DESCRIPTION> and L</SECURITY CONSIDERATIONS> for what
the URL can hold; in particular, the query cannot hold C<+>, which some
read as a space and others as a plus sign: write C<%20> or C<%2B>;

=item C<headers>

the headers of the request, as a hash reference or an array reference of
pairs. A value is a byte string, or an array reference of them, and
repeated names are joined with a comma; C<undef> values are skipped.
Names must be HTTP tokens (RFC 9110: letters, digits and
C<!#$%&'*+-.^_`|~>) and values cannot hold CR, LF or NUL characters:
anything else is an error, as it could inject headers or change the
meaning of the canonical request. The same check applies to the values
that C<sign> adds, e.g. the session token, and to the ones that
L</presign> puts in the query string instead.

An C<X-Amz-Content-Sha256> header is taken as the payload hash; if the
payload hash also comes from the arguments below, the two must agree;

=item C<body>, C<body_fh>

the payload, as a byte string, a reference to one (which avoids copying

lib/AWS/Signature/V4.pod  view on Meta::CPAN

from its current position to its end, without loading it in memory, and
put back where it was: so it must be seekable (a file, not a pipe or a
socket) and binary, without layers like C<:encoding> or C<:crlf> that
change the bytes read. Only one of them can be used;

=item C<payload_hash>, C<unsigned_payload>

skip the calculation and use the provided SHA-256 in hex, or
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.

lib/AWS/Signature/V4.pod  view on Meta::CPAN

Only C<host> and the C<x-amz-*> headers are signed by default, other
headers must be named in C<signed_headers> (and provided). The session
token and, for the X.509 variant, the certificate and chain travel as
query parameters. The payload is C<UNSIGNED-PAYLOAD> for S3 and the hash
of the empty body for other services, unless C<body>, C<body_fh>,
C<payload_hash>, C<unsigned_payload> or an C<X-Amz-Content-Sha256>
header say otherwise. The service cannot know about a payload hash that
it does not take for granted (see C<payload_header> in L</new>): in that
case it goes in the C<x-amz-content-sha256> header, signed, which the
client must send. Streaming does not apply, and its arguments are an
error.

The URL must not already carry any of the parameters that C<presign> adds
(C<X-Amz-Algorithm>, C<X-Amz-Credential>, C<X-Amz-Date>,
C<X-Amz-Expires>, C<X-Amz-SignedHeaders>, C<X-Amz-Signature>,
C<X-Amz-Security-Token>, C<X-Amz-X509>, C<X-Amz-X509-Chain>, in any
case): it is an error, so that the URL cannot end up with two values for
the same parameter.

The returned hash reference has the C<url> to hand out, with any
character that is not allowed in a URL path (e.g. a space)
percent-encoded, the C<headers> that the client must send along (just
C<host> by default), and
C<signature>, C<signed_headers>, C<scope>, C<canonical_request> and
C<string_to_sign> like in L</sign>.

=head2 encoded_length

   my $length = AWS::Signature::V4->encoded_length($decoded, $chunk_size, %opts);

Class method that computes the size of an C<aws-chunked> body made of
C<$decoded> bytes of data cut in chunks of C<$chunk_size> bytes (except
for the last one, that can be shorter), which is what to put in the
C<Content-Length> header. The sizes must be integers, and unknown
options are an error. The options mirror the choices made for L</sign>:

=over

=item C<signed>

true (C<1>, default) if the chunks are signed, false (C<0> or the empty
string) otherwise; C<signed> and C<unsigned> are also accepted, as for
C<streaming> in L</sign>;

=item C<checksum>

the name of the built-in checksum, if any;

=item C<trailers>

hash reference from the name of each trailer that will be given to
L</finish> to the length of its value (e.g. C<12> for a base64-encoded
CRC-64). As in L</sign>, the trailer of C<checksum> must not be listed
here as well: declaring the same name twice is an error.

=back

=head2 The chunker

When streaming, L</sign> returns a C<chunker> that wraps the data
into chunks, signing them if needed. It keeps track of the amount of
data and of the signatures, so use it for one body only, and feed it the
data in order.

=head3 chunk

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

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

=head3 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 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

lib/AWS/Signature/V4.pod  view on Meta::CPAN


=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.

lib/AWS/Signature/V4/Checksum.pod  view on Meta::CPAN

check against L</is_supported_algorithm> if you call this directly.

=head2 add

   $sum->add(\$bytes);

Update the checksum with a piece of data. The argument is a I<reference>
to a byte string, to avoid copying data around. Return the object, so
calls can be chained.

It is an error (an L<Ouch> exception with code C<400>) to pass anything
else than a reference to a defined string, or a string with characters
that do not fit in a byte: encode them first, e.g. with
C<Encode::encode('UTF-8', $text)>.

=head2 base64

   my $value = $sum->base64;

Return the checksum of what was added so far, as base64 of the raw
(big-endian) checksum without line breaks, as AWS wants it. It does not

lib/AWS/Signature/V4/Chunker.pm  view on Meta::CPAN

   fail 400, 'chunker already finished' if $self->_done;
   fail 400, "got @{[ $self->_seen ]} bytes instead of the declared @{[ $self->expected ]}"
      if $self->_seen != $self->expected;
   my %values;
   for my $name (sort keys %given) {
      fail 400, "trailer '" . shown(lc $name) . "' given twice" if exists $values{lc $name};
      $values{lc $name} = $given{$name};
   }

   # all checks come before the checksum is taken, which is not repeatable,
   # and before the chain of signatures moves on: after an error, the chunker
   # is as it was
   my $computed = $self->_sum ? 'x-amz-checksum-' . $self->checksum : undef;
   my @pairs;
   for my $name ($self->trailer_names->@*) {
      my $value;
      if (defined $computed && $name eq $computed) {
         fail 400, "trailer '$name' is computed, not to be given" if exists $values{$name};
      }
      else {
         $value = delete $values{$name} // fail 400, "missing value for trailer '$name'";

lib/AWS/Signature/V4/Chunker.pod  view on Meta::CPAN


=head1 INTERFACE

=head2 chunk

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

Return the encoded version of a piece of data, to be sent as it is.
C<$data> is a byte string or a reference to one (to avoid copying large
pieces), it cannot be empty. Other kinds of references, objects included,
are refused. It is an error to pass characters that do not
fit in a byte, or to go past the C<decoded_content_length> declared to
C<sign>. Pieces must be passed in the order they are sent.

If a C<checksum> was requested, the piece also updates the running
checksum.

=head2 finish

   my $encoded = $chunker->finish(%trailer_values);

Return the final, empty chunk, followed by the trailers if any, and
their signature for the signed variants. It is an error if the amount of
data passed so far is not the declared C<decoded_content_length>, or if
the chunker was already finished.

The values of the trailers declared with C<trailers> are passed by name,
in any case (C<X-Foo> and C<x-foo> are the same trailer). It is an error
to omit one, to pass one twice, to pass one that was not declared or that
the chunker computes by itself (see C<checksum>), or to pass a value that
is not a byte string or has a CR, LF or NUL in it. The trailer of a
C<checksum> is added automatically.

All the errors are found before anything changes: after one, the
chunker is as it was, and C<finish> can be called again with good
values.

=head2 new

Internal, see L</DESCRIPTION>.


=head1 AUTHOR

lib/AWS/Signature/V4/Error.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::Error - Throw the errors of AWS::Signature::V4


=head1 VERSION

This module is part of the L<AWS::Signature::V4> distribution and shares
its version.


=head1 SYNOPSIS

lib/AWS/Signature/V4/Error.pod  view on Meta::CPAN

   fail 400, 'bad option', {option => 'foo', got => $value};    # data for logs


=head1 DESCRIPTION

B<This module is internal: you are not supposed to use it.> It is what the
modules of the distribution use to throw L<Ouch> exceptions, see
L<AWS::Signature::V4/ERRORS> for what the callers can expect.

It exists to do two things that a bare C<ouch> would not do: report the
error at the line of the caller, like C<croak>, and keep the arguments of
the calls out of the backtrace kept by the exception, because they can
include secrets like passwords and tokens.


=head1 INTERFACE

=head2 fail

   fail $code, $message;
   fail $code, $message, $data;

Throw an L<Ouch> exception. All arguments go to C<ouch> as they are.

The code is C<400> when the problem is in what the caller provided, C<500>
when it is in the module itself.

The optional C<$data> is kept by the exception and is available with
C<< $@->data >> (and C<< $@->hashref >>), but it is not part of the
message, so it is not shown by just printing the error. Use it for the
details that are useful in a log, e.g. what was received, and keep it
out of what is shown to end users. It is also kept out of the backtrace:
the exception stores C<$data> itself, but the arguments of the calls are
not recorded in C<< $@->trace >>, so they can safely carry secrets.

Nothing in the distribution passes C<$data> at the moment, but it is part
of the interface and you can rely on it being passed through.


=head2 shown

lib/AWS/Signature/V4/X509.pm  view on Meta::CPAN

sub _certificate_serial ($der) {
   my (undef, $cs) = _tlv($der, 0);
   my (undef, $ts) = _tlv($der, $cs);
   my ($tag, $s, $e) = _tlv($der, $ts);
   ($tag, $s, $e) = _tlv($der, $e) if $tag == 0xA0;    # skip explicit version
   fail 400, 'cannot find certificate serial number' unless $tag == 0x02;
   return Math::BigInt->from_hex(unpack 'H*', substr $der, $s, $e - $s)
      ->bstr;   # DER integers here are positive; leading 0x00 is harmless
}

# The error of CryptX, without the location and the backtrace that Carp adds
# when verbose (the arguments in it may hold the password), escaped as caller
# input because it can hold the file name.
sub _clean_error ($e) {
   $e =~ s{\n\t.*}{}s;    # the backtrace lines start with a tab
   $e =~ s{\s+at \S+ line \d+\.?\s*\z}{};
   return shown($e =~ s{\A\s+|\s+\z}{}gr);
}

# $key is a file name or a scalar reference to the PEM text
sub _cryptx_signer ($algo, $key, $password = undef) {
   my $class = $algo eq 'RSA' ? 'Crypt::PK::RSA' : 'Crypt::PK::ECC';
   eval "require $class; 1" or fail 500, "cannot load $class";
   my @args = ($key, defined $password ? ($password) : ());
   my $pk = eval {
      # no backtrace, e.g. from -MCarp=verbose or Carp::Always, with the password
      local $Carp::Verbose = 0;
      local $Carp::MaxArgNums = -1;
      local $SIG{__DIE__};
      $class->new(@args);
   } // fail 400, "cannot load the private key: " . _clean_error($@);
   fail 400, 'the key is not a private key' unless $pk->is_private;
   return $algo eq 'RSA'
      ? sub ($bytes) { $pk->sign_message($bytes, 'SHA256', 'v1.5') }
      : sub ($bytes) { $pk->sign_message($bytes, 'SHA256') };
}

use namespace::clean;

# The certificate-based variant, as used by IAM Roles Anywhere. The options
# are the ones of the "x509" option of AWS::Signature::V4:

lib/AWS/Signature/V4/X509.pod  view on Meta::CPAN

name AWS knows. The class is documented for those who work on the
distribution, not as a public interface.

The constructor loads and checks everything: it reads the files, decodes
the certificates (PEM or DER) and finds the serial number, and loads the
key with L<CryptX> unless a C<signer> is provided. Any problem is reported
by an L<Ouch> exception with code C<400>, so it is seen when the signer is
created and not when the first request is signed. It does not check that
the certificate is valid otherwise, or that the key matches it: only
that each certificate is a DER SEQUENCE (definite, minimal lengths) that
fills its data exactly. The error of L<CryptX> about the key goes in the
message without its location or backtrace, which may hold the password,
and escaped, as it may hold the file name.

C<private_key> and C<private_key_password> have no public accessor and are
dropped as soon as the key is loaded, so that the object does not keep
them around.


=head1 THE INTERFACE OF A VARIANT

script/aws-iam-ra  view on Meta::CPAN



=head1 DESCRIPTION

Program to fetch credentials for assuming a specific role using AWS IAM
Roles Anywhere. It's meant as a replacement for C<aws_credential_helper>.

It's basically everything in the example.

You can restrict the duration by providing option C<--duration>, use a value
that is at least 900 or you will get an error.

By default it prints out only the credentials; you can use C<--full> to
print the whole response from AWS.

The key type is assumed to be C<RSA> by default. Set it to C<ECDSA> in case
you are using the other type.

=head1 CONFIGURATION

Most of the command-line parameters can be set using environment variables:

t/canonical.t  view on Meta::CPAN


sub canon ($signer, $url, %rest) {
   my $r = $signer->sign(method => 'GET', url => $url, time => 1440938160, %rest);
   my @lines = split /\n/, $r->{canonical_request}, -1;
   return ($lines[1], $lines[2], $r);    # path, query, whole result
}
sub path_of  ($signer, $url) { (canon($signer, $url))[0] }
sub query_of ($signer, $url) { (canon($signer, $url))[1] }
sub req (@args) { (canon(@args))[2] }

# errors caused by the caller: Ouch exceptions with code 400
sub bad ($re = qr/./) {
   object { prop blessed => 'Ouch'; call code => 400; call message => match $re };
}

subtest 'path, non-S3 (normalized, double-encoded)' => sub {
   my %case = (    # from the AWS SigV4 test suite
      'https://h'                      => '/',
      'https://h/'                     => '/',
      'https://h/example/..'           => '/',
      'https://h/./'                   => '/',

t/canonical.t  view on Meta::CPAN

      'authorization header';

   my $t = AWS::Signature::V4->new(service => 'service', region => 'r',
      credentials => {%cred, session_token => 'TOK'});
   $r = $t->sign(method => 'GET', url => 'https://h/', time => 0);
   is $r->{headers}{'x-amz-security-token'}, 'TOK', 'session token header';
   like $r->{signed_headers}, qr/x-amz-security-token/, 'is signed';
   like $r->{headers}{'x-amz-date'}, qr/^19700101T000000Z$/, 'epoch 0';
};

subtest 'constructor errors' => sub {
   is dies { AWS::Signature::V4->new(region => 'r', credentials => {%cred}) },
      bad(qr/service/), 'no service';
   is dies { AWS::Signature::V4->new(service => 's', credentials => {%cred}) },
      bad(qr/region/), 'no region';
   is dies { AWS::Signature::V4->new(service => 's', region => 'r') },
      bad(qr/credentials/), 'no credentials';
   is dies { AWS::Signature::V4->new(service => 's', region => 'r', credentials => {access_key_id => 'x'}) },
      bad(qr/secret_access_key/), 'missing secret';
   is dies { AWS::Signature::V4->new(service => 's', region => 'r', credentials => {%cred, token => 'T'}) },
      bad(qr/unknown option "token" in credentials/), 'unknown credentials option';

t/error.t  view on Meta::CPAN

   is $e->data, undef, 'no data by default';
   is thrown(500, 'internal')->code, 500, 'other code';
};

subtest 'the third argument is passed down' => sub {
   my $e = thrown(400, 'bad option', {option => 'foo', got => $secret});
   is $e->code, 400, 'code';
   is $e->message, 'bad option', 'message';
   is $e->data, {option => 'foo', got => $secret}, 'data is available';
   is $e->hashref->{data}, {option => 'foo', got => $secret}, 'and it is in hashref too';
   unlike "$e", qr{\Q$secret\E|foo}, 'data is not in the stringified error';
   unlike $e->message, qr{\Q$secret\E}, 'nor in the message';

   my $d = [1, 2, 3];
   is thrown(400, 'array', $d)->data, [1, 2, 3], 'not only hashes';
   is thrown(400, 'scalar', 'plain')->data, 'plain', 'nor references';
};

subtest 'data and arguments stay out of the trace' => sub {
   my $e = do {
      package Some::Caller;
      sub run ($password) { main::thrown(400, 'oops', {got => $secret}) }
      run('Hunter2');
   };
   is $e->data, {got => $secret}, 'data kept';
   unlike $e->trace, qr{\Q$secret\E|Hunter2|HASH\(}, 'no arguments in the trace';
};

subtest 'reported where the caller is' => sub {
   my $line = __LINE__ + 1;
   my $e = dies { fail 400, 'here', {some => 'data'} };
   like "$e", qr{ at \S*error\.t line $line\b}, 'file and line of the caller';
};

done_testing;

t/errors.t  view on Meta::CPAN

sub ouch_like ($code, $status, $name) {
   is dies { $code->() },
      object { prop blessed => 'Ouch'; call code => $status },
      $name;
}

sub streaming ($signer, %opts) {
   $signer->sign(method => 'PUT', url => 'http://h/', streaming => 1, %opts);
}

# errors caused by the caller are Ouch exceptions with code 400
my %caller = (
   'no service'      => sub { AWS::Signature::V4->new(region => 'r', %cred) },
   'no region'       => sub { AWS::Signature::V4->new(service => 's', %cred) },
   'no credentials'  => sub { AWS::Signature::V4->new(service => 's', region => 'r') },
   'both variants'   => sub { AWS::Signature::V4->new(service => 's', region => 'r', %cred, x509 => {}) },
   'bad key type'   => sub { AWS::Signature::V4->new(service => 's', region => 'r', x509 => {key_type => 'DSA'}) },
   'x509 key type under its old name' =>
      sub { AWS::Signature::V4->new(service => 's', region => 'r', x509 => {algorithm => 'RSA'}) },
   'credentials not a hash' =>
      sub { AWS::Signature::V4->new(service => 's', region => 'r', credentials => 'AKID:SECRET') },

t/errors.t  view on Meta::CPAN

      my $ck = streaming($s, decoded_content_length => 0)->{chunker};
      $ck->finish;
      $ck->finish;
   },
);
ouch_like $caller{$_}, 400, $_ for sort keys %caller;

# ... including what goes wrong when loading certificates and keys
my %x509 = (service => 's', region => 'r');
my $der = "\x30\x03\x02\x01\x05";    # a tiny, valid, DER structure
my %x509_errors = (
   'garbage certificate' => {key_type => 'RSA', certificate => 'garbage', serial => 1, signer => sub { 'x' }},
   'truncated certificate' => {key_type => 'RSA', certificate => "\x30\x82\x01", signer => sub { 'x' }},
   'signer not code' => {key_type => 'RSA', certificate => $der, serial => 1, signer => 'nope'},
   'missing key file' => {key_type => 'RSA', certificate => $der, serial => 1, private_key_file => '/no/such/key'},
   'garbage key' => {key_type => 'ECDSA', certificate => $der, serial => 1, private_key => 'garbage'},
);
for my $name (sort keys %x509_errors) {
   ouch_like sub { AWS::Signature::V4->new(%x509, x509 => $x509_errors{$name}) }, 400, $name;
}

# internal problems are 500
ouch_like sub { AWS::Signature::V4::Chunker->new(signed => 1, expected => 0) }, 500,
   'chunker without key';

# the error is reported where the caller is, and holds no secrets
my $e = dies {
   AWS::Signature::V4->new(%x509, x509 => {
      key_type => 'RSA', certificate => $der, serial => 1,
      private_key => 'garbage', private_key_password => $secret,
   });
};
like "$e", qr{ at \S*errors\.t line \d+}, 'error reported at the line of the caller';
unlike $e->trace, qr{\Q$secret\E}, 'the trace does not include the password';
unlike "$e", qr{\Q$secret\E}, 'the message does not include the password';
for my $how ('verbose', 'die handler') {
   my $e = dies {
      local $Carp::Verbose = $how eq 'verbose';
      local $SIG{__DIE__} = sub { die Carp::longmess($_[0]) } if $how eq 'die handler';
      AWS::Signature::V4->new(%x509, x509 => {
         key_type => 'RSA', certificate => $der, serial => 1,
         private_key => 'garbage', private_key_password => $secret,
      });
   };
   like $e->message, qr{\Acannot load the private key: [^\n]+\z},
      "$how: the message is one line";
   unlike "$e", qr{\Q$secret\E}, "$how: the message does not include the password";
}
like dies { $s->sign(url => 'http://h/') }, qr{ at \S*errors\.t line \d+},
   'same for the errors of sign';

# options: undefined is like missing, derived values cannot be set
my $t = AWS::Signature::V4->new(
   service => 's3', region => 'r', %cred,
   payload_header => undef, double_encode => undef, normalize_path => undef,
);
ok $t->payload_header, 'undefined payload_header: default for s3';
ok !$t->double_encode, 'undefined double_encode: default for s3';
ok !$t->normalize_path, 'undefined normalize_path: default for s3';
like $t->sign(method => 'GET', url => 'http://h/')->{headers},

t/errors.t  view on Meta::CPAN

   extends 'AWS::Signature::V4';
   has '+region' => (lazy => 1, builder => 1);
   sub _build_region { $_ = 'eu-west-1'; return $_ }
}
is(Sub::Signer->new(service => 's', %cred)->region, 'eu-west-1', 'builders can use $_');

# no warnings for what used to be accepted silently
is warns { $s->sign(method => 'GET', url => 'http://h/', headers => {'X-Opt' => undef}) },
   0, 'undefined header value: no warnings';

# the chunker is not confused by its own errors
my $ck = streaming($s, decoded_content_length => 9, checksum => 'sha256',
   trailers => ['x-foo'], signed_headers => ['host'])->{chunker};
$ck->chunk('123456789');
ouch_like sub { $ck->finish }, 400, 'missing trailer value';
like $ck->finish('x-foo' => 1),
   qr{x-amz-checksum-sha256:\Q@{[ encode_base64(sha256('123456789'), '') ]}\E\r\n},
   'the checksum survives a failed finish';

$ck = streaming($s, decoded_content_length => 4)->{chunker};
$ck->chunk('abc');

t/security.t  view on Meta::CPAN

   like $s->sign(method => 'GET', url => 'https://h/', time => 0)->{signature},
      qr/\A[0-9a-f]+\z/, 'signing still works';
};

subtest 'userinfo in the url is refused' => sub {
   for my $u ('http://a@evil.example@good.example/', 'http://user@good.example/') {
      is dies { $s3->sign(method => 'GET', url => $u) }, bad_request(qr/user/), $u;
   }
};

subtest 'caller input is escaped in error messages' => sub {
   for my $case (
      [sub { AWS::Signature::V4->new(service => 's', region => 'r',
         x509 => {key_type => "DSA\nFAKE LOG LINE"}) }, 'key_type'],
      [sub { $s3->sign(method => 'PUT', url => $url, streaming => 1,
         decoded_content_length => 1, checksum => "md5\nFAKE") }, 'checksum'],
      [sub { $s3->sign(method => 'PUT', url => $url, streaming => 1,
         decoded_content_length => 1, trailers => ["x\nFAKE"]) }, 'trailer name'],
      [sub { $s3->sign(method => 'GET', url => $url, signed_headers => ["x\nFAKE"]) },
         'signed header name'],
      [sub { AWS::Signature::V4->new(service => 's', region => 'r',

t/trailer.t  view on Meta::CPAN

         like $ck->finish('x-foo' => 'ok'), qr/\r\nx-foo:ok\r\n/, '... and the chunker can still finish';
      }
   }
   my $upgraded = "\xe9";
   utf8::upgrade($upgraded);
   my $body = $mk->()->finish('x-foo' => $upgraded);
   ok !utf8::is_utf8($body), 'a value stored as UTF-8 gives a byte string';
   is $body, "0\r\nx-amz-checksum-crc32:$expect{crc32}\r\nx-foo:\xe9\r\n\r\n", '... with the same bytes';
}

# --- configuration errors
ok dies { start($s3, streaming => 'unsigned') }, 'unsigned without trailers';
ok dies { start($s3, streaming => 1, checksum => 'md5') }, 'unknown checksum';
ok dies { start($s3, streaming => 1, checksum => 'crc32', trailers => ['x-amz-checksum-crc32']) }, 'same trailer twice';
ok dies { start($s3, streaming => 1, trailers => ['bad name']) }, 'invalid trailer name';
ok dies { start($s3, checksum => 'crc32') }, 'checksum without streaming';
ok dies { start($s3, trailers => ['x-foo']) }, 'trailers without streaming';
ok dies { start($s3, streaming => 'sideways') }, 'unknown streaming mode';
ok dies { start($x, streaming => 'signed', checksum => 'crc32') }, 'signed chunks need credentials';
ok lives { start($x, streaming => 'unsigned', checksum => 'crc32') }, 'unsigned works with x509';

t/variants.t  view on Meta::CPAN

      [chain => {leaf => $chain}], [chain => [$chain, undef]], [chain => [\$chain]],
      [chain_files => {leaf => '/x'}], [chain_files => [undef]],
   ) {
      my ($name, $value) = @$case;
      is dies { AWS::Signature::V4::X509->new(key_type => 'RSA', %x, $name => $value) },
         bad_request("x509/$name"), "$name: " . (ref $value eq 'HASH' ? 'hash' : 'bad item');
   }
   is warns { my $e = dies { AWS::Signature::V4::X509->new(key_type => 'RSA', %x, chain => [undef]) } },
      0, 'undefined chain item: no warnings';

   my %length_errors = (
      'indefinite length'   => "\x30\x80",
      'long form under 128' => "\x30\x81\x03\x02\x01\x05",
      'leading zero byte'   => "\x30\x82\x00\x03\x02\x01\x05",
      'length of 5 bytes'   => "\x30\x85\x00\x00\x00\x00\x03\x02\x01\x05",
   );
   for my $name (sort keys %length_errors) {
      my $bad = $length_errors{$name};
      is dies { AWS::Signature::V4::X509->new(key_type => 'RSA', %x, certificate => $bad) },
         bad_request('not a DER-encoded certificate'), "certificate, $name";
      is dies { AWS::Signature::V4::X509->new(key_type => 'RSA', %x, chain => [$bad]) },
         bad_request('not a DER-encoded certificate'), "chain, $name";
   }
   my $long = "\x30\x81\x80" . ("\x05\x00" x 64);    # long form, when needed
   is [AWS::Signature::V4::X509->new(key_type => 'RSA', %x, certificate => $long)->extra_fields],
      ['X-Amz-X509' => encode_base64($long, '')], 'minimal long form accepted';

   for my $case ([undef, 'undef'], ['', 'empty'], [[1], 'a reference'], ["\x{263a}", 'wide']) {



( run in 1.320 second using v1.01-cache-2.11-cpan-10df4c37326 )