AWS-Signature-V4
view release on metacpan or search on metacpan
lib/AWS/Signature/V4/X509.pm view on Meta::CPAN
open my $fh, '<:raw', $file or fail 400, 'open(' . shown($file) . "): $!";
local $/;
my $data = <$fh>;
close $fh;
return $data;
}
sub _to_ders ($cert) { # PEM bundles hold many certificates
return _check_der($cert) unless $cert =~ m{-----BEGIN};
my @ders = map { decode_base64($_) }
$cert =~ m{-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----}sg;
@ders or fail 400, 'no CERTIFICATE block in PEM';
return map { _check_der($_) } @ders;
}
sub _to_der ($cert) {
return _check_der($cert) unless $cert =~ m{-----BEGIN};
$cert =~ m{-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----}s
or fail 400, 'no CERTIFICATE block in PEM';
return _check_der(decode_base64($1));
}
# a certificate is a DER SEQUENCE that fits exactly in the data
sub _check_der ($der) {
my ($tag, undef, $end) = _tlv($der, 0);
fail 400, 'not a DER-encoded certificate'
unless $tag == 0x30 && $end == length $der;
return $der;
}
# read one DER TLV at $off => (tag, content_start, content_end)
sub _tlv ($der, $off) {
my $max = length $der;
$off + 2 <= $max or fail 400, 'truncated certificate';
my $tag = ord substr $der, $off++, 1;
my $len = ord substr $der, $off++, 1;
if ($len & 0x80) { # long form, DER wants it minimal and definite
my $n = $len & 0x7F;
fail 400, 'not a DER-encoded certificate' if $n == 0 || $n > 4;
$off + $n <= $max or fail 400, 'truncated certificate';
$len = 0;
$len = ($len << 8) | ord substr($der, $off++, 1) for 1 .. $n;
fail 400, 'not a DER-encoded certificate'
if $len < 0x80 || $len < 1 << 8 * ($n - 1);
}
$off + $len <= $max or fail 400, 'truncated certificate';
return ($tag, $off, $off + $len);
}
# Certificate ::= SEQ { tbs SEQ { [0] version?, INTEGER serial, ... }, ... }
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:
# certificate / certificate_file: PEM or DER text, or path of a file with it;
# only the first certificate of a PEM bundle is used
# chain / chain_files: optional arrayref of PEM/DER intermediate certificates,
# or of paths of files with them; a PEM item can be a bundle.
# chain can also be a plain string: a PEM-encoded bundle, and
# chain_files a plain string: the path of one file
# key_type: 'RSA' or 'ECDSA'
# serial: the serial number of the certificate, if it cannot be found
# signer: sub ($bytes_to_sign) -> raw signature bytes (DER for ECDSA,
# PKCS#1 v1.5 for RSA), computed with SHA-256; or, alternatively,
# private_key_file / private_key: PEM or DER key (path or content, the
# content wins), signed with CryptX; private_key_password if it
# is encrypted
has [qw<
key_type certificate certificate_file chain chain_files serial signer
private_key_file
>] => (is => 'ro');
# the key text and its password are secrets, only needed to load the key:
# no public accessor, and they are dropped as soon as the key is loaded
has $_ => (is => 'ro', reader => "_$_", clearer => "_clear_$_")
for qw< private_key private_key_password >;
# Derived from the options above. Lazy, so that each builder can use the
# others without depending on the order in which the constructor sets things
# up; BUILD forces them all, so that a bad certificate, chain or key is
# reported by new() and not by the first signature.
#
# **NOTE**: to add one, add its name here and write its builder.
my @DERIVED = qw< algorithm credential_id _chain _der _signer _type >;
has $_ => (is => 'lazy', init_arg => undef) for @DERIVED;
sub BUILD ($self, $args) {
my %known = map { $_ => 1 } qw<
key_type certificate certificate_file chain chain_files serial signer
private_key_file private_key private_key_password
>;
if (my ($name) = sort(grep { !$known{$_} } keys $args->%*)) {
fail 400, 'unknown option "' . shown($name) . '" in x509';
}
for my $name (@DERIVED) { # not "for (...)": builders may use $_
$self->$name;
}
# we don't need private key material past this point, so we get rid
# of it immediately. Of course it may still linger in memory depending
# on garbage collection.
$self->_clear_private_key;
$self->_clear_private_key_password;
return;
( run in 0.699 second using v1.01-cache-2.11-cpan-10df4c37326 )