AWS-Signature-V4

 view release on metacpan or  search on metacpan

t/variants.t  view on Meta::CPAN

use v5.24;
use Test2::V0;
use FindBin '$Bin';
use lib "$Bin/../lib";
use experimental 'signatures';
use MIME::Base64 qw< encode_base64 >;
use AWS::Signature::V4;
use AWS::Signature::V4::Credentials;
use AWS::Signature::V4::X509;

sub bad_request ($name) {
   object { prop blessed => 'Ouch'; call code => 400; call message => match qr/\Q$name\E/ };
}

subtest 'credentials' => sub {
   my $c = AWS::Signature::V4::Credentials->new(
      access_key_id => 'AKIDEXAMPLE',
      secret_access_key => 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY',
   );
   is $c->algorithm, 'AWS4-HMAC-SHA256', 'algorithm';
   is $c->credential_id, 'AKIDEXAMPLE', 'credential id is the access key';
   ok $c->can_sign_chunks, 'it can sign chunks';
   is [$c->extra_fields], [], 'no token, no extra fields';

   my $scope = '20150830/us-east-1/iam/aws4_request';
   is unpack('H*', $c->signing_key($scope)),
      'c4afb1cc5771d871763a393e44b703571b55cc28424d1a5e86da6ed3c154a4b9',
      'signing key, from the AWS documentation';
   like $c->signature($scope, 'anything'), qr/\A[0-9a-f]{64}\z/, 'hex signature';

   my $t = AWS::Signature::V4::Credentials->new(
      access_key_id => 'a', secret_access_key => 'b', session_token => 'TOKEN');
   is [$t->extra_fields], ['X-Amz-Security-Token' => 'TOKEN'], 'the session token';
   $t = AWS::Signature::V4::Credentials->new(
      access_key_id => 'a', secret_access_key => 'b', session_token => '');
   is [$t->extra_fields], [], 'an empty session token is no token';
   my $s = AWS::Signature::V4->new(service => 'sts', region => 'us-east-1',
      credentials => {access_key_id => 'a', secret_access_key => 'b', session_token => ''});
   my $r = $s->sign(method => 'GET', url => 'https://sts.amazonaws.com/', time => 0);
   ok !exists $r->{headers}{'x-amz-security-token'}, 'no empty token header';
   unlike $r->{signed_headers}, qr/security-token/, 'no empty token signed';
   unlike $s->presign(url => 'https://sts.amazonaws.com/', time => 0)->{url},
      qr/Security-Token/, 'no empty token presigned';

   is dies { AWS::Signature::V4::Credentials->new(secret_access_key => 'b') },
      bad_request('credentials/access_key_id'), 'access key is needed';
   is dies { AWS::Signature::V4::Credentials->new(access_key_id => 'a') },
      bad_request('credentials/secret_access_key'), 'secret is needed';
   is dies { AWS::Signature::V4::Credentials->new(access_key_id => '', secret_access_key => 'b') },
      bad_request('credentials/access_key_id'), 'access key cannot be empty';
   is dies { AWS::Signature::V4::Credentials->new(access_key_id => 'a', secret_access_key => '') },
      bad_request('credentials/secret_access_key'), 'secret cannot be empty';
};

subtest 'x509' => sub {
   my $der   = "\x30\x03\x02\x01\x05";     # tiny but well-formed DER
   my $chain = "\x30\x03\x02\x01\x06";
   my %x = (certificate => $der, serial => 7, signer => sub { "\x01\x02" });

   my $x = AWS::Signature::V4::X509->new(key_type  => 'ecdsa', %x);
   is $x->algorithm, 'AWS4-X509-ECDSA-SHA256', 'algorithm, from the key type';
   is $x->credential_id, 7, 'credential id is the serial';
   ok !$x->can_sign_chunks, 'it cannot sign chunks';
   is dies { $x->signing_key('20150830/us-east-1/iam/aws4_request') },
      bad_request('credentials variant'), 'no derived key';
   is $x->signature('any/scope', 'anything'), '0102', 'the signature is the hex of the signer output';
   is [$x->extra_fields], ['X-Amz-X509' => encode_base64($der, '')], 'the certificate';

   $x = AWS::Signature::V4::X509->new(key_type  => 'RSA', %x, chain => [$chain, $chain]);
   is [$x->extra_fields], [
      'X-Amz-X509' => encode_base64($der, ''),
      'X-Amz-X509-Chain' => join(',', (encode_base64($chain, '')) x 2),
   ], 'the certificate and the chain';

   for my $case (
      [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']) {
      my ($out, $name) = @$case;
      my $y = AWS::Signature::V4::X509->new(key_type => 'RSA', %x, signer => sub { $out });
      is dies { $y->signature('any/scope', 'anything') },
         bad_request('x509 signer returned'), "signer returns $name";
   }
   is dies { AWS::Signature::V4->new(service => 'iam', region => 'r',
      x509 => {key_type => 'RSA', %x, signer => sub { undef }})
      ->sign(method => 'GET', url => 'https://h/') },
      bad_request('x509 signer returned no signature'), 'sign reports the signer failure';
   my $z = AWS::Signature::V4::X509->new(key_type => 'RSA', %x, signer => sub { "\x{e9}" });
   is $z->signature('any/scope', 'anything'), 'e9', 'characters under 256 are bytes';

   is dies { AWS::Signature::V4::X509->new(%x) },
      bad_request('x509/key_type'), 'key type is needed';
   is dies { AWS::Signature::V4::X509->new(%x, key_type  => 'DSA') },
      bad_request('RSA or ECDSA'), 'key type is checked';
   is dies { AWS::Signature::V4::X509->new(key_type  => 'RSA', signer => sub { 1 }) },
      bad_request('certificate'), 'certificate is needed';
   is dies { AWS::Signature::V4::X509->new(key_type  => 'RSA', certificate => $der, serial => 1) },
      bad_request('signer'), 'a way to sign is needed';
};

subtest 'the main class gives the same results as the variants' => sub {
   my %cred = (access_key_id => 'AKID', secret_access_key => 'secret');
   my $s = AWS::Signature::V4->new(service => 'iam', region => 'us-east-1', credentials => {%cred});
   my $r = $s->sign(method => 'GET', url => 'https://iam.amazonaws.com/', time => 1440938160);
   my $c = AWS::Signature::V4::Credentials->new(%cred);
   is $r->{signature}, $c->signature($r->{scope}, $r->{string_to_sign}), 'credentials signature';
   is $s->algorithm, $c->algorithm, 'credentials algorithm';

   my $x = AWS::Signature::V4->new(
      service => 'iam', region => 'us-east-1',
      x509 => {key_type  => 'RSA', certificate => "\x30\x03\x02\x01\x05", serial => 9,
               signer => sub { 'sig' }});
   $r = $x->sign(method => 'GET', url => 'https://iam.amazonaws.com/', time => 1440938160);
   is $r->{signature}, unpack('H*', 'sig'), 'x509 signature';
   like $r->{authorization}, qr{Credential=9/20150830/us-east-1/iam/aws4_request}, 'x509 credential';
   ok exists $r->{headers}{'x-amz-x509'}, 'the certificate header';
   is $x->algorithm, 'AWS4-X509-RSA-SHA256', 'x509 algorithm';
};

done_testing;



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