AWS-Signature-V4

 view release on metacpan or  search on metacpan

t/trailer.t  view on Meta::CPAN

use v5.24;
use experimental 'signatures';
use Test2::V0;
use FindBin '$Bin';
use lib "$Bin/../lib";
use AWS::Signature::V4;
use Digest::SHA qw< sha1 sha256 sha256_hex hmac_sha256 hmac_sha256_hex >;
use MIME::Base64 qw< encode_base64 >;

my $s3 = AWS::Signature::V4->new(service => 's3', region => 'us-east-1', credentials => {
      access_key_id => 'AKIAIOSFODNN7EXAMPLE',
      secret_access_key => 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'});
my $x = AWS::Signature::V4->new(service => 's3', region => 'r', x509 => {
      key_type  => 'RSA', serial => 1, signer => sub { 'x' },
      certificate => "-----BEGIN CERTIFICATE-----\nMAMCAQc=\n-----END CERTIFICATE-----\n"});

sub bad ($re) {
   object { prop blessed => 'Ouch'; call code => 400; call message => match $re };
}

# the reference for the signed chunks and trailers
my $key = 'AWS4' . 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
$key = hmac_sha256($_, $key) for '20130524', 'us-east-1', 's3', 'aws4_request';
my $scope = '20130524/us-east-1/s3/aws4_request';
my $sts = sub ($kind, @parts) { join "\n", "AWS4-HMAC-SHA256-$kind", '20130524T000000Z', $scope, @parts };

sub start ($signer = $s3, %opts) {
   $signer->sign(method => 'PUT', url => 'https://h/k', time => 1369353600,
      decoded_content_length => 9, %opts);
}

# --- checksums, fed in two pieces
my %expect = (    # of "123456789"
   crc32  => encode_base64(pack('N', 0xCBF43926), ''),
   crc32c => encode_base64(pack('N', 0xE3069283), ''),
   sha1   => encode_base64(sha1('123456789'), ''),
   sha256 => encode_base64(sha256('123456789'), ''),
);
for my $algo (sort keys %expect) {
   my $ck = start($x, streaming => 'unsigned', checksum => $algo)->{chunker};
   my $body = $ck->chunk('1234') . $ck->chunk('56789') . $ck->finish;
   is $body, "4\r\n1234\r\n5\r\n56789\r\n0\r\nx-amz-checksum-$algo:$expect{$algo}\r\n\r\n",
      "unsigned, $algo, byte for byte";
}

# --- crc32c against a bitwise reference, on longer data and in many chunks
sub crc32c_ref ($data) {
   my $crc = 0xFFFFFFFF;
   for my $byte (unpack 'C*', $data) {
      $crc ^= $byte;
      $crc = ($crc & 1) ? (($crc >> 1) ^ 0x82F63B78) : ($crc >> 1) for 1 .. 8;
   }
   return encode_base64(pack('N', $crc ^ 0xFFFFFFFF), '');
}
my $long = join '', map { chr(($_ * 7) % 256) } 1 .. 5000;
my $ck = start($x, streaming => 'unsigned', checksum => 'crc32c',
   decoded_content_length => length $long)->{chunker};
$ck->chunk(substr($long, $_ * 1000, 1000)) for 0 .. 4;
like $ck->finish, qr/x-amz-checksum-crc32c:\Q${\ crc32c_ref($long)}\E\r\n/, 'crc32c on longer data';

# the test here will exercise whatever is available; we don't explicitly
# test the pure-perl version shipped with the module in case
# String::CRC32 is available. FIXME figure out how to do this withouth
# going back to using package variables.
for my $size (1 .. 9, 65535 .. 65537, 70001) {
   my $sum = AWS::Signature::V4::Checksum->new('crc32c');
   my $data = substr $long x 15, 0, $size;
   my ($head, $tail) = (substr($data, 0, $size / 3), substr($data, $size / 3));
   $sum->add(\$head)->add(\$tail);
   is $sum->base64, crc32c_ref($data), "crc32c, $size bytes";

t/trailer.t  view on Meta::CPAN

   $ck->chunk('123456789');
   is $ck->finish('x-amz-checksum-crc64nvme' => 'AAAAAAAAAAA='),
      "0\r\nx-amz-checksum-crc64nvme:AAAAAAAAAAA=\r\n\r\n", 'value given at finish';

   $r = start($s3, streaming => 1, checksum => 'sha1', trailers => ['x-foo', 'x-bar']);
   is $r->{headers}{'x-amz-trailer'}, 'x-amz-checksum-sha1,x-foo,x-bar', 'built-in first, then declared';
   $ck = $r->{chunker};
   $ck->chunk('123456789');
   my $sig1 = hmac_sha256_hex($sts->('PAYLOAD', $r->{signature}, sha256_hex(''), sha256_hex('123456789')), $key);
   my $sig2 = hmac_sha256_hex($sts->('PAYLOAD', $sig1, sha256_hex(''), sha256_hex('')), $key);
   my @lines = ("x-amz-checksum-sha1:$expect{sha1}", 'x-foo:1', 'x-bar:2');
   my $tsig = hmac_sha256_hex($sts->('TRAILER', $sig2, sha256_hex(join '', map { "$_\n" } @lines)), $key);
   is $ck->finish('x-foo' => '1', 'x-bar' => '2'),
      "0;chunk-signature=$sig2\r\n" . join('', map { "$_\r\n" } @lines)
      . "x-amz-trailer-signature:$tsig\r\n\r\n",
      'all trailers, in order, signed';

   my $mk = sub ($signer = $x) {
      my $c = start($signer, streaming => $signer == $x ? 'unsigned' : 1, checksum => 'crc32',
         trailers => ['X-Foo'])->{chunker};
      $c->chunk('123456789');
      $c;
   };
   is dies { $mk->()->finish }, bad(qr/missing value for trailer 'x-foo'/), 'missing value';
   is dies { $mk->()->finish('x-foo' => 1, 'x-nope' => 2) }, bad(qr/undeclared trailers: x-nope/),
      'undeclared value';
   is dies { $mk->()->finish('x-foo' => 1, 'x-amz-checksum-crc32' => 'x') },
      bad(qr/'x-amz-checksum-crc32' is computed/), 'computed value given';
   is dies { $mk->()->finish('x-foo' => 1, 'X-FOO' => 2) }, bad(qr/'x-foo' given twice/),
      'same trailer twice, in different cases';
   is $mk->()->finish('X-Foo' => 1), "0\r\nx-amz-checksum-crc32:$expect{crc32}\r\nx-foo:1\r\n\r\n",
      'names are not case sensitive';

   # a bad value leaves the chunker as it was, signed or not
   my %bad_value = ("a\nb" => 'newline', "a\rb" => 'carriage return', "a\0b" => 'NUL',
      "\x{263a}" => 'wide character', 'ref' => 'reference');
   for my $signer ($x, $s3) {
      for my $value (sort keys %bad_value) {
         my $ck = $mk->($signer);
         is dies { $ck->finish('x-foo' => $value eq 'ref' ? {} : $value) },
            bad(qr/invalid value for trailer 'x-foo'/),
            ($signer == $x ? 'unsigned' : 'signed') . ": $bad_value{$value} in value";
         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';

# --- encoded_length equals the real length
my @cases = (
   [signed => 1, {}],
   [signed => 1, {checksum => 'crc32c'}, {streaming => 1, checksum => 'crc32c'}],
   [signed => 1, {trailers => {'x-amz-checksum-crc64nvme' => 12}}, {streaming => 1, trailers => ['x-amz-checksum-crc64nvme']}, {'x-amz-checksum-crc64nvme' => 'A' x 12}],
   [signed => 0, {checksum => 'sha256'}, {streaming => 'unsigned', checksum => 'sha256'}],
   [signed => 0, {checksum => 'sha1', trailers => {'x-foo' => 3}}, {streaming => 'unsigned', checksum => 'sha1', trailers => ['x-foo']}, {'x-foo' => 'abc'}],
);
for my $case (@cases) {
   my ($label, $signed, $opts, $sign, $values) = @$case;
   $sign //= {streaming => 1};
   for my $size (0, 1, 15, 16, 17, 255, 256, 300, 5000) {
      for my $chunk (16, 256, 1000) {
         my $c = start($signed ? $s3 : $x, %$sign, decoded_content_length => $size)->{chunker};
         my $data = 'z' x $size;
         my $body = '';
         for (my $off = 0; $off < $size; $off += $chunk) {
            $body .= $c->chunk(substr($data, $off, $chunk));
         }
         $body .= $c->finish(%{$values // {}});
         is length($body), AWS::Signature::V4->encoded_length($size, $chunk, signed => $signed, %$opts),
            "encoded_length: " . join(',', $signed ? 'signed' : 'unsigned', sort keys %$opts) . " size $size chunk $chunk";
      }
   }
}
is(AWS::Signature::V4->encoded_length(66560, 65536), 66824, 'still the AWS docs value without trailers');
ok dies { AWS::Signature::V4->encoded_length(10, 5, checksum => 'nope') }, 'encoded_length: unknown checksum';

done_testing;



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