File Coverage

blib/lib/HTTP/PublicKeyPins.pm
Criterion Covered Total %
statement 121 131 92.3
branch 36 54 66.6
condition 3 3 100.0
subroutine 23 24 95.8
pod 1 1 100.0
total 184 213 86.3


line stmt bran cond sub pod time code
1             package HTTP::PublicKeyPins;
2              
3 2     2   43474 use 5.006;
  2         7  
4 2     2   11 use strict;
  2         4  
  2         42  
5 2     2   9 use warnings;
  2         12  
  2         53  
6 2     2   1559 use Crypt::OpenSSL::X509();
  2         10016  
  2         64  
7 2     2   1637 use Crypt::OpenSSL::RSA();
  2         11023  
  2         48  
8 2     2   1475 use Crypt::OpenSSL::PKCS10();
  2         1424  
  2         45  
9 2     2   1522 use Convert::ASN1();
  2         87378  
  2         53  
10 2     2   2544 use Digest();
  2         998  
  2         38  
11 2     2   1540 use MIME::Base64();
  2         2476  
  2         64  
12 2     2   1512 use English qw( -no_match_vars );
  2         8014  
  2         10  
13 2     2   2286 use FileHandle();
  2         22769  
  2         48  
14 2     2   14 use Exporter();
  2         3  
  2         31  
15 2     2   9 use Carp();
  2         3  
  2         3807  
16             *import = \&Exporter::import;
17             our @EXPORT_OK = qw(
18             pin_sha256
19             );
20             our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK, );
21              
22 28     28   595 sub _CERTIFICATE_HEADER_SIZE { return 40; }
23 22     22   391 sub _MAX_PUBLIC_KEY_SIZE { return 65_536; }
24              
25             our $VERSION = '0.12';
26              
27             sub pin_sha256 {
28 29     29 1 624987 my ($path) = @_;
29 29 100       830 my $handle = FileHandle->new($path)
30             or Carp::croak("Failed to open $path for reading:$EXTENDED_OS_ERROR");
31 28         5629 binmode $handle;
32 28 50       226 read $handle, my $file_header, _CERTIFICATE_HEADER_SIZE()
33             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
34 28         70 my $pem_encoded_public_key_string;
35 28 100       834 if ( $file_header =~
    100          
    100          
36             /^[-]{5}BEGIN[ ](?:X[.]?509[ ]|TRUSTED[ ])?CERTIFICATE[-]{5}/smx )
37             {
38 5         27 $pem_encoded_public_key_string =
39             _process_pem_x509_certificate( $handle, $file_header, $path );
40             }
41             elsif ( $file_header =~
42             /^[-]{5}BEGIN[ ](?:RSA[ ])?(PUBLIC|PRIVATE)[ ]KEY[-]{5}/smx )
43             {
44 8         155 my ($type) = ($1);
45 8 50       79 if ( $type eq 'PRIVATE' ) {
46 0         0 $pem_encoded_public_key_string =
47             _process_pem_private_key( $handle, $file_header, $path );
48             }
49             else {
50 8         101 $pem_encoded_public_key_string =
51             _process_pem_public_key( $handle, $file_header, $path );
52             }
53             }
54             elsif ( $file_header =~
55             /^[-]{5}BEGIN[ ](?:NEW[ ])?CERTIFICATE[ ]REQUEST[-]{5}/smx )
56             {
57 4         29 $pem_encoded_public_key_string =
58             _process_pem_pkcs10_certificate_request( $handle, $file_header,
59             $path );
60             }
61             else {
62 11   100     70 $pem_encoded_public_key_string =
63             _check_for_der_encoded_x509_certificate( $handle, $file_header,
64             $path )
65             || _check_for_der_encoded_private_key( $handle, $file_header, $path )
66             || _check_for_der_encoded_public_key( $handle, $file_header, $path );
67 11 100       61 if ( !defined $pem_encoded_public_key_string ) {
68 1         162 Carp::croak("$path is not an X.509 Certificate");
69             }
70             }
71              
72 27         379 $pem_encoded_public_key_string =~
73             s/^[-]{5}BEGIN[ ]PUBLIC[ ]KEY[-]{5}\r?\n//smx;
74 27         336 $pem_encoded_public_key_string =~
75             s/^[-]{5}END[ ]PUBLIC[ ]KEY[-]{5}\r?\n//smx;
76 27         298 my $der_encoded_public_key_string =
77             MIME::Base64::decode($pem_encoded_public_key_string);
78 27         476 my $digest = Digest->new('SHA-256');
79 27         36443 $digest->add($der_encoded_public_key_string);
80 27         478 my $base64 = MIME::Base64::encode_base64( $digest->digest() );
81 27         118 chomp $base64;
82 27         690 return $base64;
83             }
84              
85             sub _process_pem_x509_certificate {
86 5     5   17 my ( $handle, $file_header, $path ) = @_;
87 5         12 my $pem_encoded_public_key_string;
88 5 100       45 if ( $file_header =~ /^[-]{5}BEGIN[ ]CERTIFICATE[-]{5}/smx ) {
89 3         994 my $x509 = Crypt::OpenSSL::X509->new_from_file($path);
90 3         33 $pem_encoded_public_key_string =
91             _get_pem_encoded_public_key_string($x509);
92             }
93             else {
94 2 50       37 seek $handle, 0, Fcntl::SEEK_SET()
95             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
96 2 50       27 defined read $handle, my $pem_encoded_certificate_string,
97             _MAX_PUBLIC_KEY_SIZE()
98             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
99 2         83 $pem_encoded_certificate_string =~
100             s/^([-]{5}BEGIN[ ])(?:X[.]?509|TRUSTED)[ ](CERTIFICATE[-]{5})/$1$2/smx;
101 2         53 $pem_encoded_certificate_string =~
102             s/^([-]{5}END[ ])(?:X[.]?509|TRUSTED)[ ](CERTIFICATE[-]{5})/$1$2/smx;
103 2         354 my $x509 = Crypt::OpenSSL::X509->new_from_string(
104             $pem_encoded_certificate_string);
105 2         26 $pem_encoded_public_key_string =
106             _get_pem_encoded_public_key_string($x509);
107             }
108 5         36 return $pem_encoded_public_key_string;
109             }
110              
111             sub _process_pem_pkcs10_certificate_request {
112 4     4   15 my ( $handle, $file_header, $path ) = @_;
113 4         709 my $req = Crypt::OpenSSL::PKCS10->new_from_file($path);
114 4         241 my $pem_encoded_public_key_string = $req->get_pem_pubkey();
115 4         111 return $pem_encoded_public_key_string;
116             }
117              
118             sub _check_for_der_encoded_x509_certificate {
119 11     11   55 my ( $handle, $file_header, $path ) = @_;
120 11         24 my $pem_encoded_public_key_string;
121             eval {
122 11         2187 my $x509 = Crypt::OpenSSL::X509->new_from_file( $path,
123             Crypt::OpenSSL::X509::FORMAT_ASN1() );
124 5         57 $pem_encoded_public_key_string =
125             _get_pem_encoded_public_key_string($x509);
126 11 100       55 } or do {
127 6         104 return;
128             };
129 5         61 return $pem_encoded_public_key_string;
130             }
131              
132             sub _check_for_der_encoded_public_key {
133 6     6   25 my ( $handle, $file_header, $path ) = @_;
134 6         13 my $pem_encoded_public_key_string;
135 6 50       37 seek $handle, 0, Fcntl::SEEK_SET()
136             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
137 6 50       26 defined read $handle, my $der_encoded_public_key_string,
138             _MAX_PUBLIC_KEY_SIZE()
139             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
140 6         167 my $asn = Convert::ASN1->new( encoding => 'DER' );
141 6 50       505 $asn->prepare(
142             <<"__SUBJECT_PUBLIC_KEY_INFO__") or Carp::croak( 'Failed to prepare SubjectPublicKeyInfo in ASN1:' . $asn->error() );
143             SEQUENCE {
144             algorithm SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY OPTIONAL },
145             subjectPublicKey BIT STRING
146             }
147             __SUBJECT_PUBLIC_KEY_INFO__
148             eval {
149 6 100       71 my $pub_key = $asn->decode($der_encoded_public_key_string)
150             or Carp::croak(
151             'Failed to decode SubjectPublicKeyInfo in ASN1:' . $asn->error() );
152 5         2078 $pem_encoded_public_key_string =
153             "-----BEGIN PUBLIC KEY-----\n"
154             . MIME::Base64::encode_base64($der_encoded_public_key_string)
155             . "-----END PUBLIC KEY-----\n";
156 6 100       15467 } or do {
157 1         359 return;
158             };
159 5         53 return $pem_encoded_public_key_string;
160             }
161              
162             sub _check_for_der_encoded_private_key {
163 6     6   109 my ( $handle, $file_header, $path ) = @_;
164 6         17 my $pem_encoded_public_key_string;
165 6 50       56 seek $handle, 0, Fcntl::SEEK_SET()
166             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
167 6 50       50 defined read $handle, my $der_encoded_private_key_string,
168             _MAX_PUBLIC_KEY_SIZE()
169             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
170 6         66 my $pem_encoded_private_key_string =
171             "-----BEGIN RSA PRIVATE KEY-----\n"
172             . MIME::Base64::encode_base64($der_encoded_private_key_string)
173             . "-----END RSA PRIVATE KEY-----\n";
174             eval {
175 6         448 my $privkey =
176             Crypt::OpenSSL::RSA->new_private_key($pem_encoded_private_key_string);
177 0         0 $pem_encoded_public_key_string = $privkey->get_public_key_x509_string();
178 6 50       22 } or do {
179 6         68 return;
180             };
181 0         0 return $pem_encoded_public_key_string;
182             }
183              
184             sub _process_pem_private_key {
185 0     0   0 my ( $handle, $file_header, $path ) = @_;
186 0         0 my $pem_encoded_public_key_string;
187 0 0       0 seek $handle, 0, Fcntl::SEEK_SET()
188             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
189 0 0       0 defined read $handle, my $rsa_private_key_string, _MAX_PUBLIC_KEY_SIZE()
190             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
191 0         0 my $privkey = Crypt::OpenSSL::RSA->new_private_key($rsa_private_key_string);
192 0         0 $pem_encoded_public_key_string = $privkey->get_public_key_x509_string();
193 0         0 return $pem_encoded_public_key_string;
194             }
195              
196             sub _process_pem_public_key {
197 8     8   42 my ( $handle, $file_header, $path ) = @_;
198 8         29 my $pem_encoded_public_key_string;
199 8 100       114 if ( $file_header =~ /^[-]{5}BEGIN[ ]RSA[ ]PUBLIC[ ]KEY[-]{5}/smx ) {
200 1 50       39 seek $handle, 0, Fcntl::SEEK_SET()
201             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
202 1 50       13 defined read $handle, my $pem_encoded_rsa_public_key_string,
203             _MAX_PUBLIC_KEY_SIZE()
204             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
205 1         92 my $pubkey = Crypt::OpenSSL::RSA->new_public_key(
206             $pem_encoded_rsa_public_key_string);
207 1         132 $pem_encoded_public_key_string = $pubkey->get_public_key_x509_string();
208             }
209             else {
210 7 50       101 seek $handle, 0, Fcntl::SEEK_SET()
211             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
212 7 50       71 defined read $handle, $pem_encoded_public_key_string,
213             _MAX_PUBLIC_KEY_SIZE()
214             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
215             }
216 8         99 return $pem_encoded_public_key_string;
217             }
218              
219             sub _get_pem_encoded_public_key_string {
220 10     10   36 my ($x509) = @_;
221 10         82 my $pem_encoded_public_key_string;
222 10 100       234 if ( $x509->key_alg_name() eq 'rsaEncryption' ) {
223 4         625 my $pubkey = Crypt::OpenSSL::RSA->new_public_key( $x509->pubkey() );
224 4         1438 $pem_encoded_public_key_string = $pubkey->get_public_key_x509_string();
225             }
226             else {
227 6         1214 $pem_encoded_public_key_string = $x509->pubkey();
228             }
229 10         336 return $pem_encoded_public_key_string;
230             }
231              
232             1; # End of HTTP::PublicKeyPins
233             __END__