File Coverage

blib/lib/HTTP/PublicKeyPins.pm
Criterion Covered Total %
statement 150 150 100.0
branch 51 72 70.8
condition 3 3 100.0
subroutine 25 25 100.0
pod 1 1 100.0
total 230 251 91.6


line stmt bran cond sub pod time code
1             package HTTP::PublicKeyPins;
2              
3 2     2   189098 use 5.006;
  2         12  
4 2     2   12 use strict;
  2         4  
  2         58  
5 2     2   11 use warnings;
  2         5  
  2         76  
6 2     2   1030 use Crypt::OpenSSL::X509();
  2         8349  
  2         88  
7 2     2   982 use Crypt::OpenSSL::RSA();
  2         9108  
  2         53  
8 2     2   756 use Crypt::PKCS10();
  2         52815  
  2         47  
9 2     2   17 use Convert::ASN1();
  2         4  
  2         28  
10 2     2   1114 use Digest();
  2         1133  
  2         42  
11 2     2   21 use MIME::Base64();
  2         6  
  2         39  
12 2     2   1031 use English qw( -no_match_vars );
  2         7466  
  2         11  
13 2     2   1671 use FileHandle();
  2         20543  
  2         60  
14 2     2   17 use Exporter();
  2         4  
  2         30  
15 2     2   9 use Carp();
  2         5  
  2         4943  
16             *import = \&Exporter::import;
17             our @EXPORT_OK = qw(
18             pin_sha256
19             );
20             our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK, );
21              
22 34     34   644 sub _CERTIFICATE_HEADER_SIZE { return 40; }
23 44     44   884 sub _MAX_PUBLIC_KEY_SIZE { return 65_536; }
24              
25             our $VERSION = '0.16';
26              
27             sub pin_sha256 {
28 35     35 1 409790 my ($path) = @_;
29 35 100       986 my $handle = FileHandle->new($path)
30             or Carp::croak("Failed to open $path for reading:$EXTENDED_OS_ERROR");
31 34         7820 binmode $handle;
32 34 50       168 read $handle, my $file_header, _CERTIFICATE_HEADER_SIZE()
33             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
34 34         116 my $pem_encoded_public_key_string;
35 34 100       1029 if ( $file_header =~
    100          
    100          
36             /^[-]{5}BEGIN[ ](?:X[.]?509[ ]|TRUSTED[ ])?CERTIFICATE[-]{5}/smx )
37             {
38 5         109 $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 9         201 my ($type) = ($1);
45 9 100       124 if ( $type eq 'PRIVATE' ) {
46 1         32 $pem_encoded_public_key_string =
47             _process_pem_private_key( $handle, $file_header, $path );
48             }
49             else {
50 8         145 $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         66 $pem_encoded_public_key_string =
58             _process_pem_pkcs10_certificate_request( $handle, $file_header,
59             $path );
60             }
61             else {
62 16   100     165 $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_pkcs10_certificate_request( $handle, $file_header,
67             $path )
68             || _check_for_der_encoded_public_key( $handle, $file_header, $path );
69 16 100       85 if ( !defined $pem_encoded_public_key_string ) {
70 1         116 Carp::croak("$path is not an X.509 Certificate");
71             }
72             }
73              
74 33         556 $pem_encoded_public_key_string =~
75             s/^[-]{5}BEGIN[ ]PUBLIC[ ]KEY[-]{5}\r?\n//smx;
76 33         512 $pem_encoded_public_key_string =~
77             s/^[-]{5}END[ ]PUBLIC[ ]KEY[-]{5}\r?\n//smx;
78 33         427 my $der_encoded_public_key_string =
79             MIME::Base64::decode($pem_encoded_public_key_string);
80 33         686 my $digest = Digest->new('SHA-256');
81 33         7586 $digest->add($der_encoded_public_key_string);
82 33         344 my $base64 = MIME::Base64::encode_base64( $digest->digest() );
83 33         121 chomp $base64;
84 33         1006 return $base64;
85             }
86              
87             sub _process_pem_x509_certificate {
88 5     5   52 my ( $handle, $file_header, $path ) = @_;
89 5         28 my $pem_encoded_public_key_string;
90 5 100       85 if ( $file_header =~ /^[-]{5}BEGIN[ ]CERTIFICATE[-]{5}/smx ) {
91 3         640 my $x509 = Crypt::OpenSSL::X509->new_from_file($path);
92 3         54 $pem_encoded_public_key_string =
93             _get_pem_encoded_public_key_string($x509);
94             }
95             else {
96 2 50       63 seek $handle, 0, Fcntl::SEEK_SET()
97             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
98 2 50       78 defined read $handle, my $pem_encoded_certificate_string,
99             _MAX_PUBLIC_KEY_SIZE()
100             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
101 2         99 $pem_encoded_certificate_string =~
102             s/^([-]{5}BEGIN[ ])(?:X[.]?509|TRUSTED)[ ](CERTIFICATE[-]{5})/$1$2/smx;
103 2         40 $pem_encoded_certificate_string =~
104             s/^([-]{5}END[ ])(?:X[.]?509|TRUSTED)[ ](CERTIFICATE[-]{5})/$1$2/smx;
105 2         239 my $x509 = Crypt::OpenSSL::X509->new_from_string(
106             $pem_encoded_certificate_string);
107 2         30 $pem_encoded_public_key_string =
108             _get_pem_encoded_public_key_string($x509);
109             }
110 5         27 return $pem_encoded_public_key_string;
111             }
112              
113             sub _process_pem_pkcs10_certificate_request {
114 4     4   31 my ( $handle, $file_header, $path ) = @_;
115 4 50       73 seek $handle, 0, Fcntl::SEEK_SET()
116             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
117 4 50       32 defined read $handle, my $pkcs10_certificate_string, _MAX_PUBLIC_KEY_SIZE()
118             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
119 4         103 Crypt::PKCS10->setAPIversion(1);
120 4 50       2547 my $req = Crypt::PKCS10->new($pkcs10_certificate_string)
121             or Carp::croak( 'Failed to initialise Crypt::PKCS10 library:'
122             . Crypt::PKCS10->error() );
123 4         1345915 my $pem_encoded_public_key_string = $req->subjectPublicKey(1);
124 4         171 return $pem_encoded_public_key_string;
125             }
126              
127             sub _check_for_der_pkcs10_certificate_request {
128 10     10   49 my ( $handle, $file_header, $path ) = @_;
129 10         36 my $pem_encoded_public_key_string;
130             eval {
131 10 50       102 seek $handle, 0, Fcntl::SEEK_SET()
132             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
133 10 50       46 defined read $handle, my $pkcs10_certificate_string,
134             _MAX_PUBLIC_KEY_SIZE()
135             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
136 10         223 Crypt::PKCS10->setAPIversion(1);
137 10 100       5478 my $req = Crypt::PKCS10->new($pkcs10_certificate_string)
138             or Carp::croak( 'Failed to initialise Crypt::PKCS10 library:'
139             . Crypt::PKCS10->error() );
140 2         636713 $pem_encoded_public_key_string = $req->subjectPublicKey(1);
141 10 100       23 } or do {
142 8         390072 return;
143             };
144 2         137 return $pem_encoded_public_key_string;
145             }
146              
147             sub _check_for_der_encoded_x509_certificate {
148 16     16   119 my ( $handle, $file_header, $path ) = @_;
149 16         64 my $pem_encoded_public_key_string;
150             eval {
151 16 50       135 my $temp_handle = FileHandle->new( $path, Fcntl::O_RDONLY() )
152             or
153             Carp::croak("Failed to open '$path' for reading:$EXTENDED_OS_ERROR");
154 16         1702 binmode $temp_handle;
155 16         40 my $string;
156 16         399 while ( my $line = <$temp_handle> ) {
157 71         368 $string .= $line;
158             }
159 16 50       273 close $temp_handle
160             or Carp::croak("Failed to close '$path':$EXTENDED_OS_ERROR");
161 16         1371 my $x509 = Crypt::OpenSSL::X509->new_from_string( $string,
162             Crypt::OpenSSL::X509::FORMAT_ASN1() );
163 5         60 $pem_encoded_public_key_string =
164             _get_pem_encoded_public_key_string($x509);
165 16 100       81 } or do {
166 11         186 return;
167             };
168 5         40 return $pem_encoded_public_key_string;
169             }
170              
171             sub _check_for_der_encoded_public_key {
172 8     8   36 my ( $handle, $file_header, $path ) = @_;
173 8         23 my $pem_encoded_public_key_string;
174 8 50       129 seek $handle, 0, Fcntl::SEEK_SET()
175             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
176 8 50       48 defined read $handle, my $der_encoded_public_key_string,
177             _MAX_PUBLIC_KEY_SIZE()
178             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
179 8         93 my $asn = Convert::ASN1->new( encoding => 'DER' );
180 8 50       509 $asn->prepare(
181             <<"__SUBJECT_PUBLIC_KEY_INFO__") or Carp::croak( 'Failed to prepare SubjectPublicKeyInfo in ASN1:' . $asn->error() );
182             SEQUENCE {
183             algorithm SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY OPTIONAL },
184             subjectPublicKey BIT STRING
185             }
186             __SUBJECT_PUBLIC_KEY_INFO__
187             eval {
188 8 100       38 my $pub_key = $asn->decode($der_encoded_public_key_string)
189             or Carp::croak(
190             'Failed to decode SubjectPublicKeyInfo in ASN1:' . $asn->error() );
191 7         2303 $pem_encoded_public_key_string =
192             "-----BEGIN PUBLIC KEY-----\n"
193             . MIME::Base64::encode_base64($der_encoded_public_key_string)
194             . "-----END PUBLIC KEY-----\n";
195 8 100       16818 } or do {
196 1         260 return;
197             };
198 7         53 return $pem_encoded_public_key_string;
199             }
200              
201             sub _check_for_der_encoded_private_key {
202 11     11   77 my ( $handle, $file_header, $path ) = @_;
203 11         20 my $pem_encoded_public_key_string;
204 11 50       160 seek $handle, 0, Fcntl::SEEK_SET()
205             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
206 11 50       54 defined read $handle, my $der_encoded_private_key_string,
207             _MAX_PUBLIC_KEY_SIZE()
208             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
209 11         152 my $pem_encoded_private_key_string =
210             "-----BEGIN RSA PRIVATE KEY-----\n"
211             . MIME::Base64::encode_base64($der_encoded_private_key_string)
212             . "-----END RSA PRIVATE KEY-----\n";
213             eval {
214 11         670 my $privkey =
215             Crypt::OpenSSL::RSA->new_private_key($pem_encoded_private_key_string);
216 1         77 $pem_encoded_public_key_string = $privkey->get_public_key_x509_string();
217 11 100       37 } or do {
218 10         139 return;
219             };
220 1         23 return $pem_encoded_public_key_string;
221             }
222              
223             sub _process_pem_private_key {
224 1     1   12 my ( $handle, $file_header, $path ) = @_;
225 1         7 my $pem_encoded_public_key_string;
226 1 50       43 seek $handle, 0, Fcntl::SEEK_SET()
227             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
228 1 50       23 defined read $handle, my $rsa_private_key_string, _MAX_PUBLIC_KEY_SIZE()
229             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
230 1         98 my $privkey = Crypt::OpenSSL::RSA->new_private_key($rsa_private_key_string);
231 1         105 $pem_encoded_public_key_string = $privkey->get_public_key_x509_string();
232 1         26 return $pem_encoded_public_key_string;
233             }
234              
235             sub _process_pem_public_key {
236 8     8   39 my ( $handle, $file_header, $path ) = @_;
237 8         23 my $pem_encoded_public_key_string;
238 8 100       89 if ( $file_header =~ /^[-]{5}BEGIN[ ]RSA[ ]PUBLIC[ ]KEY[-]{5}/smx ) {
239 1 50       33 seek $handle, 0, Fcntl::SEEK_SET()
240             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
241 1 50       24 defined read $handle, my $pem_encoded_rsa_public_key_string,
242             _MAX_PUBLIC_KEY_SIZE()
243             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
244 1         176 my $pubkey = Crypt::OpenSSL::RSA->new_public_key(
245             $pem_encoded_rsa_public_key_string);
246 1         146 $pem_encoded_public_key_string = $pubkey->get_public_key_x509_string();
247             }
248             else {
249 7 50       134 seek $handle, 0, Fcntl::SEEK_SET()
250             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
251 7 50       176 defined read $handle, $pem_encoded_public_key_string,
252             _MAX_PUBLIC_KEY_SIZE()
253             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
254             }
255 8         99 return $pem_encoded_public_key_string;
256             }
257              
258             sub _get_pem_encoded_public_key_string {
259 10     10   56 my ($x509) = @_;
260 10         22 my $pem_encoded_public_key_string;
261 10 100       200 if ( $x509->key_alg_name() eq 'rsaEncryption' ) {
262 4         611 my $pubkey = Crypt::OpenSSL::RSA->new_public_key( $x509->pubkey() );
263 4         1487 $pem_encoded_public_key_string = $pubkey->get_public_key_x509_string();
264             }
265             else {
266 6         846 $pem_encoded_public_key_string = $x509->pubkey();
267             }
268 10         307 return $pem_encoded_public_key_string;
269             }
270              
271             1; # End of HTTP::PublicKeyPins
272             __END__