AWS-Signature-V4

 view release on metacpan or  search on metacpan

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

#!/usr/bin/env perl
# The X.509 variant: instead of a secret key, the request is signed with
# the private key of a certificate, as IAM Roles Anywhere wants, to get
# temporary credentials.
#
#    CERT_FILE=cert.pem KEY_FILE=key.pem KEY_TYPE=RSA \
#    TRUST_ANCHOR_ARN=... PROFILE_ARN=... ROLE_ARN=... ./07-rolesanywhere-x509.pl
#
# Optional: CHAIN_FILE (PEM bundle of intermediate CAs), KEY_PASSWORD (for
# encrypted keys), AWS_REGION. KEY_TYPE is RSA (default) or ECDSA.
#
# The session goes on the standard output and nothing else does, so that
# it can be piped into jq or saved for later use. It holds temporary
# credentials: a file it is saved in deserves the same care as a key.
#
# NOTE: check the shape of the CreateSession request (path, body) against
# the current IAM Roles Anywhere API reference before relying on it.
use v5.24;
use warnings;
use FindBin '$Bin';
use lib "$Bin/../lib";
use AWS::Signature::V4;
use HTTP::Tiny;
use JSON::PP qw< encode_json >;

my $region = $ENV{AWS_REGION} // 'us-east-1';

# the region goes in the host name below, so a "/" in it moves the host
# elsewhere: "evil.example.com/x" makes the URL
# https://rolesanywhere.evil.example.com/x.amazonaws.com/sessions, whose
# host is rolesanywhere.evil.example.com. The request carries the
# certificate and its signature, and whatever answered would be written
# out as if it were a session
die "invalid region: letters, digits and dashes only\n"
   unless $region =~ m{\A[a-z0-9-]+\z};

my $signer = AWS::Signature::V4->new(
   service => 'rolesanywhere',
   region  => $region,
   x509    => {
      key_type         => $ENV{KEY_TYPE} // 'RSA',
      certificate_file => $ENV{CERT_FILE} // die("CERT_FILE?\n"),
      private_key_file => $ENV{KEY_FILE}  // die("KEY_FILE?\n"),
      (defined $ENV{CHAIN_FILE}   ? (chain_files          => $ENV{CHAIN_FILE})   : ()),
      (defined $ENV{KEY_PASSWORD} ? (private_key_password => $ENV{KEY_PASSWORD}) : ()),
   },
);
say {*STDERR} 'algorithm: ', $signer->algorithm;

# the ARNs are required; only a dry run, which sends nothing, makes do
# with a placeholder just to show the shape of the request
sub arn {
   my ($name, $placeholder) = @_;
   return $ENV{$name} if defined $ENV{$name} && length $ENV{$name};
   die "$name?\n" unless $ENV{DRY_RUN};
   return $placeholder;
}

my $url  = "https://rolesanywhere.$region.amazonaws.com/sessions";
my $body = encode_json({
   trustAnchorArn  => arn(TRUST_ANCHOR_ARN => 'arn:aws:rolesanywhere:...:trust-anchor/...'),
   profileArn      => arn(PROFILE_ARN      => 'arn:aws:rolesanywhere:...:profile/...'),
   roleArn         => arn(ROLE_ARN         => 'arn:aws:iam::...:role/...'),
   durationSeconds => 3600,
});
my $r = $signer->sign(
   method  => 'POST',
   url     => $url,
   headers => {'Content-Type' => 'application/json'},
   body    => $body,
);
# 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};



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