HTTP-PublicKeyPins

 view release on metacpan or  search on metacpan

lib/HTTP/PublicKeyPins.pm  view on Meta::CPAN

package HTTP::PublicKeyPins;

use 5.006;
use strict;
use warnings;
use Crypt::OpenSSL::X509();
use Crypt::OpenSSL::RSA();
use Crypt::PKCS10();
use Convert::ASN1();
use Digest();
use MIME::Base64();
use English qw( -no_match_vars );
use FileHandle();
use Exporter();
use Carp();
*import = \&Exporter::import;
our @EXPORT_OK = qw(
  pin_sha256
);
our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK, );

sub _CERTIFICATE_HEADER_SIZE { return 40; }
sub _MAX_PUBLIC_KEY_SIZE     { return 65_536; }

our $VERSION = '0.16';

sub pin_sha256 {
    my ($path) = @_;
    my $handle = FileHandle->new($path)
      or Carp::croak("Failed to open $path for reading:$EXTENDED_OS_ERROR");
    binmode $handle;
    read $handle, my $file_header, _CERTIFICATE_HEADER_SIZE()
      or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
    my $pem_encoded_public_key_string;
    if ( $file_header =~
        /^[-]{5}BEGIN[ ](?:X[.]?509[ ]|TRUSTED[ ])?CERTIFICATE[-]{5}/smx )
    {
        $pem_encoded_public_key_string =
          _process_pem_x509_certificate( $handle, $file_header, $path );
    }
    elsif ( $file_header =~
        /^[-]{5}BEGIN[ ](?:RSA[ ])?(PUBLIC|PRIVATE)[ ]KEY[-]{5}/smx )
    {
        my ($type) = ($1);
        if ( $type eq 'PRIVATE' ) {
            $pem_encoded_public_key_string =
              _process_pem_private_key( $handle, $file_header, $path );
        }
        else {
            $pem_encoded_public_key_string =
              _process_pem_public_key( $handle, $file_header, $path );
        }
    }
    elsif ( $file_header =~
        /^[-]{5}BEGIN[ ](?:NEW[ ])?CERTIFICATE[ ]REQUEST[-]{5}/smx )
    {
        $pem_encoded_public_key_string =
          _process_pem_pkcs10_certificate_request( $handle, $file_header,
            $path );
    }
    else {
        $pem_encoded_public_key_string =
          _check_for_der_encoded_x509_certificate( $handle, $file_header,
            $path )
          || _check_for_der_encoded_private_key( $handle, $file_header, $path )
          || _check_for_der_pkcs10_certificate_request( $handle, $file_header,
            $path )
          || _check_for_der_encoded_public_key( $handle, $file_header, $path );
        if ( !defined $pem_encoded_public_key_string ) {
            Carp::croak("$path is not an X.509 Certificate");
        }
    }

    $pem_encoded_public_key_string =~
      s/^[-]{5}BEGIN[ ]PUBLIC[ ]KEY[-]{5}\r?\n//smx;
    $pem_encoded_public_key_string =~
      s/^[-]{5}END[ ]PUBLIC[ ]KEY[-]{5}\r?\n//smx;
    my $der_encoded_public_key_string =
      MIME::Base64::decode($pem_encoded_public_key_string);
    my $digest = Digest->new('SHA-256');
    $digest->add($der_encoded_public_key_string);
    my $base64 = MIME::Base64::encode_base64( $digest->digest() );
    chomp $base64;
    return $base64;
}

sub _process_pem_x509_certificate {
    my ( $handle, $file_header, $path ) = @_;
    my $pem_encoded_public_key_string;
    if ( $file_header =~ /^[-]{5}BEGIN[ ]CERTIFICATE[-]{5}/smx ) {
        my $x509 = Crypt::OpenSSL::X509->new_from_file($path);
        $pem_encoded_public_key_string =
          _get_pem_encoded_public_key_string($x509);
    }
    else {
        seek $handle, 0, Fcntl::SEEK_SET()
          or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
        defined read $handle, my $pem_encoded_certificate_string,
          _MAX_PUBLIC_KEY_SIZE()
          or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
        $pem_encoded_certificate_string =~
s/^([-]{5}BEGIN[ ])(?:X[.]?509|TRUSTED)[ ](CERTIFICATE[-]{5})/$1$2/smx;
        $pem_encoded_certificate_string =~
          s/^([-]{5}END[ ])(?:X[.]?509|TRUSTED)[ ](CERTIFICATE[-]{5})/$1$2/smx;
        my $x509 = Crypt::OpenSSL::X509->new_from_string(
            $pem_encoded_certificate_string);
        $pem_encoded_public_key_string =
          _get_pem_encoded_public_key_string($x509);
    }
    return $pem_encoded_public_key_string;
}

sub _process_pem_pkcs10_certificate_request {
    my ( $handle, $file_header, $path ) = @_;
    seek $handle, 0, Fcntl::SEEK_SET()
      or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
    defined read $handle, my $pkcs10_certificate_string, _MAX_PUBLIC_KEY_SIZE()
      or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
    Crypt::PKCS10->setAPIversion(1);
    my $req = Crypt::PKCS10->new($pkcs10_certificate_string)
      or Carp::croak( 'Failed to initialise Crypt::PKCS10 library:'
          . Crypt::PKCS10->error() );
    my $pem_encoded_public_key_string = $req->subjectPublicKey(1);
    return $pem_encoded_public_key_string;
}

lib/HTTP/PublicKeyPins.pm  view on Meta::CPAN

    } or do {
        return;
    };
    return $pem_encoded_public_key_string;
}

sub _check_for_der_encoded_x509_certificate {
    my ( $handle, $file_header, $path ) = @_;
    my $pem_encoded_public_key_string;
    eval {
        my $temp_handle = FileHandle->new( $path, Fcntl::O_RDONLY() )
          or
          Carp::croak("Failed to open '$path' for reading:$EXTENDED_OS_ERROR");
        binmode $temp_handle;
        my $string;
        while ( my $line = <$temp_handle> ) {
            $string .= $line;
        }
        close $temp_handle
          or Carp::croak("Failed to close '$path':$EXTENDED_OS_ERROR");
        my $x509 = Crypt::OpenSSL::X509->new_from_string( $string,
            Crypt::OpenSSL::X509::FORMAT_ASN1() );
        $pem_encoded_public_key_string =
          _get_pem_encoded_public_key_string($x509);
    } or do {
        return;
    };
    return $pem_encoded_public_key_string;
}

sub _check_for_der_encoded_public_key {
    my ( $handle, $file_header, $path ) = @_;
    my $pem_encoded_public_key_string;
    seek $handle, 0, Fcntl::SEEK_SET()
      or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
    defined read $handle, my $der_encoded_public_key_string,
      _MAX_PUBLIC_KEY_SIZE()
      or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
    my $asn = Convert::ASN1->new( encoding => 'DER' );
    $asn->prepare(
        <<"__SUBJECT_PUBLIC_KEY_INFO__") or Carp::croak( 'Failed to prepare SubjectPublicKeyInfo in ASN1:' . $asn->error() );
SEQUENCE {
  algorithm SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY OPTIONAL },
  subjectPublicKey BIT STRING
}
__SUBJECT_PUBLIC_KEY_INFO__
    eval {
        my $pub_key = $asn->decode($der_encoded_public_key_string)
          or Carp::croak(
            'Failed to decode SubjectPublicKeyInfo in ASN1:' . $asn->error() );
        $pem_encoded_public_key_string =
            "-----BEGIN PUBLIC KEY-----\n"
          . MIME::Base64::encode_base64($der_encoded_public_key_string)
          . "-----END PUBLIC KEY-----\n";
    } or do {
        return;
    };
    return $pem_encoded_public_key_string;
}

sub _check_for_der_encoded_private_key {
    my ( $handle, $file_header, $path ) = @_;
    my $pem_encoded_public_key_string;
    seek $handle, 0, Fcntl::SEEK_SET()
      or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
    defined read $handle, my $der_encoded_private_key_string,
      _MAX_PUBLIC_KEY_SIZE()
      or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
    my $pem_encoded_private_key_string =
        "-----BEGIN RSA PRIVATE KEY-----\n"
      . MIME::Base64::encode_base64($der_encoded_private_key_string)
      . "-----END RSA PRIVATE KEY-----\n";
    eval {
        my $privkey =
          Crypt::OpenSSL::RSA->new_private_key($pem_encoded_private_key_string);
        $pem_encoded_public_key_string = $privkey->get_public_key_x509_string();
    } or do {
        return;
    };
    return $pem_encoded_public_key_string;
}

sub _process_pem_private_key {
    my ( $handle, $file_header, $path ) = @_;
    my $pem_encoded_public_key_string;
    seek $handle, 0, Fcntl::SEEK_SET()
      or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
    defined read $handle, my $rsa_private_key_string, _MAX_PUBLIC_KEY_SIZE()
      or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
    my $privkey = Crypt::OpenSSL::RSA->new_private_key($rsa_private_key_string);
    $pem_encoded_public_key_string = $privkey->get_public_key_x509_string();
    return $pem_encoded_public_key_string;
}

sub _process_pem_public_key {
    my ( $handle, $file_header, $path ) = @_;
    my $pem_encoded_public_key_string;
    if ( $file_header =~ /^[-]{5}BEGIN[ ]RSA[ ]PUBLIC[ ]KEY[-]{5}/smx ) {
        seek $handle, 0, Fcntl::SEEK_SET()
          or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
        defined read $handle, my $pem_encoded_rsa_public_key_string,
          _MAX_PUBLIC_KEY_SIZE()
          or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
        my $pubkey = Crypt::OpenSSL::RSA->new_public_key(
            $pem_encoded_rsa_public_key_string);
        $pem_encoded_public_key_string = $pubkey->get_public_key_x509_string();
    }
    else {
        seek $handle, 0, Fcntl::SEEK_SET()
          or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
        defined read $handle, $pem_encoded_public_key_string,
          _MAX_PUBLIC_KEY_SIZE()
          or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
    }
    return $pem_encoded_public_key_string;
}

sub _get_pem_encoded_public_key_string {
    my ($x509) = @_;
    my $pem_encoded_public_key_string;
    if ( $x509->key_alg_name() eq 'rsaEncryption' ) {
        my $pubkey = Crypt::OpenSSL::RSA->new_public_key( $x509->pubkey() );
        $pem_encoded_public_key_string = $pubkey->get_public_key_x509_string();
    }
    else {
        $pem_encoded_public_key_string = $x509->pubkey();
    }
    return $pem_encoded_public_key_string;
}

1;    # End of HTTP::PublicKeyPins
__END__

=head1 NAME

HTTP::PublicKeyPins - Generate RFC 7469 HTTP Public Key Pin (HPKP) header values

=head1 VERSION

Version 0.16

=head1 SYNOPSIS

Make it more difficult for the bad guys to Man-In-The-Middle your users TLS sessions

    use HTTP::Headers();
    use HTTP::PublicKeyPins qw( pin_sha256 );

    ...
    my $h = HTTP::Headers->new();



( run in 0.452 second using v1.01-cache-2.11-cpan-13bb782fe5a )