File Coverage

blib/lib/HTTP/PublicKeyPins.pm
Criterion Covered Total %
statement 134 144 93.0
branch 43 66 65.1
condition 3 3 100.0
subroutine 24 25 96.0
pod 1 1 100.0
total 205 239 85.7


line stmt bran cond sub pod time code
1             package HTTP::PublicKeyPins;
2              
3 2     2   134878 use 5.006;
  2         15  
4 2     2   10 use strict;
  2         4  
  2         57  
5 2     2   11 use warnings;
  2         3  
  2         61  
6 2     2   1001 use Crypt::OpenSSL::X509();
  2         8044  
  2         74  
7 2     2   956 use Crypt::OpenSSL::RSA();
  2         9001  
  2         51  
8 2     2   1432 use Crypt::PKCS10();
  2         105398  
  2         73  
9 2     2   21 use Convert::ASN1();
  2         6  
  2         31  
10 2     2   1297 use Digest();
  2         1113  
  2         43  
11 2     2   21 use MIME::Base64();
  2         5  
  2         40  
12 2     2   1074 use English qw( -no_match_vars );
  2         7630  
  2         12  
13 2     2   1624 use FileHandle();
  2         20514  
  2         59  
14 2     2   16 use Exporter();
  2         4  
  2         27  
15 2     2   10 use Carp();
  2         5  
  2         4112  
16             *import = \&Exporter::import;
17             our @EXPORT_OK = qw(
18             pin_sha256
19             );
20             our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK, );
21              
22 32     32   790 sub _CERTIFICATE_HEADER_SIZE { return 40; }
23 42     42   972 sub _MAX_PUBLIC_KEY_SIZE { return 65_536; }
24              
25             our $VERSION = '0.15';
26              
27             sub pin_sha256 {
28 33     33 1 396024 my ($path) = @_;
29 33 100       986 my $handle = FileHandle->new($path)
30             or Carp::croak("Failed to open $path for reading:$EXTENDED_OS_ERROR");
31 32         7292 binmode $handle;
32 32 50       422 read $handle, my $file_header, _CERTIFICATE_HEADER_SIZE()
33             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
34 32         122 my $pem_encoded_public_key_string;
35 32 100       999 if ( $file_header =~
    100          
    100          
36             /^[-]{5}BEGIN[ ](?:X[.]?509[ ]|TRUSTED[ ])?CERTIFICATE[-]{5}/smx )
37             {
38 5         56 $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         270 my ($type) = ($1);
45 8 50       96 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         127 $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 15   100     187 $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 15 100       126 if ( !defined $pem_encoded_public_key_string ) {
70 1         101 Carp::croak("$path is not an X.509 Certificate");
71             }
72             }
73              
74 31         406 $pem_encoded_public_key_string =~
75             s/^[-]{5}BEGIN[ ]PUBLIC[ ]KEY[-]{5}\r?\n//smx;
76 31         549 $pem_encoded_public_key_string =~
77             s/^[-]{5}END[ ]PUBLIC[ ]KEY[-]{5}\r?\n//smx;
78 31         374 my $der_encoded_public_key_string =
79             MIME::Base64::decode($pem_encoded_public_key_string);
80 31         771 my $digest = Digest->new('SHA-256');
81 31         8935 $digest->add($der_encoded_public_key_string);
82 31         342 my $base64 = MIME::Base64::encode_base64( $digest->digest() );
83 31         114 chomp $base64;
84 31         958 return $base64;
85             }
86              
87             sub _process_pem_x509_certificate {
88 5     5   38 my ( $handle, $file_header, $path ) = @_;
89 5         18 my $pem_encoded_public_key_string;
90 5 100       68 if ( $file_header =~ /^[-]{5}BEGIN[ ]CERTIFICATE[-]{5}/smx ) {
91 3         597 my $x509 = Crypt::OpenSSL::X509->new_from_file($path);
92 3         64 $pem_encoded_public_key_string =
93             _get_pem_encoded_public_key_string($x509);
94             }
95             else {
96 2 50       59 seek $handle, 0, Fcntl::SEEK_SET()
97             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
98 2 50       28 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         97 $pem_encoded_certificate_string =~
102             s/^([-]{5}BEGIN[ ])(?:X[.]?509|TRUSTED)[ ](CERTIFICATE[-]{5})/$1$2/smx;
103 2         56 $pem_encoded_certificate_string =~
104             s/^([-]{5}END[ ])(?:X[.]?509|TRUSTED)[ ](CERTIFICATE[-]{5})/$1$2/smx;
105 2         227 my $x509 = Crypt::OpenSSL::X509->new_from_string(
106             $pem_encoded_certificate_string);
107 2         74 $pem_encoded_public_key_string =
108             _get_pem_encoded_public_key_string($x509);
109             }
110 5         46 return $pem_encoded_public_key_string;
111             }
112              
113             sub _process_pem_pkcs10_certificate_request {
114 4     4   53 my ( $handle, $file_header, $path ) = @_;
115 4 50       136 seek $handle, 0, Fcntl::SEEK_SET()
116             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
117 4 50       31 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         134 Crypt::PKCS10->setAPIversion(1);
120 4 50       2658 my $req = Crypt::PKCS10->new($pkcs10_certificate_string)
121             or Carp::croak( 'Failed to initialise Crypt::PKCS10 library:'
122             . Crypt::PKCS10->error() );
123 4         1342342 my $pem_encoded_public_key_string = $req->subjectPublicKey(1);
124 4         178 return $pem_encoded_public_key_string;
125             }
126              
127             sub _check_for_der_pkcs10_certificate_request {
128 10     10   45 my ( $handle, $file_header, $path ) = @_;
129 10         20 my $pem_encoded_public_key_string;
130             eval {
131 10 50       162 seek $handle, 0, Fcntl::SEEK_SET()
132             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
133 10 50       218 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         246 Crypt::PKCS10->setAPIversion(1);
137 10         5669 my $req = Crypt::PKCS10->new($pkcs10_certificate_string);
138 10         613541 $pem_encoded_public_key_string = $req->subjectPublicKey(1);
139 10 100       22 } or do {
140 8         135 return;
141             };
142 2         108 return $pem_encoded_public_key_string;
143             }
144              
145             sub _check_for_der_encoded_x509_certificate {
146 15     15   95 my ( $handle, $file_header, $path ) = @_;
147 15         68 my $pem_encoded_public_key_string;
148             eval {
149 15         1911 my $x509 = Crypt::OpenSSL::X509->new_from_file( $path,
150             Crypt::OpenSSL::X509::FORMAT_ASN1() );
151 5         72 $pem_encoded_public_key_string =
152             _get_pem_encoded_public_key_string($x509);
153 15 100       79 } or do {
154 10         182 return;
155             };
156 5         91 return $pem_encoded_public_key_string;
157             }
158              
159             sub _check_for_der_encoded_public_key {
160 8     8   45 my ( $handle, $file_header, $path ) = @_;
161 8         25 my $pem_encoded_public_key_string;
162 8 50       134 seek $handle, 0, Fcntl::SEEK_SET()
163             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
164 8 50       44 defined read $handle, my $der_encoded_public_key_string,
165             _MAX_PUBLIC_KEY_SIZE()
166             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
167 8         85 my $asn = Convert::ASN1->new( encoding => 'DER' );
168 8 50       506 $asn->prepare(
169             <<"__SUBJECT_PUBLIC_KEY_INFO__") or Carp::croak( 'Failed to prepare SubjectPublicKeyInfo in ASN1:' . $asn->error() );
170             SEQUENCE {
171             algorithm SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY OPTIONAL },
172             subjectPublicKey BIT STRING
173             }
174             __SUBJECT_PUBLIC_KEY_INFO__
175             eval {
176 8 100       34 my $pub_key = $asn->decode($der_encoded_public_key_string)
177             or Carp::croak(
178             'Failed to decode SubjectPublicKeyInfo in ASN1:' . $asn->error() );
179 7         2247 $pem_encoded_public_key_string =
180             "-----BEGIN PUBLIC KEY-----\n"
181             . MIME::Base64::encode_base64($der_encoded_public_key_string)
182             . "-----END PUBLIC KEY-----\n";
183 8 100       16307 } or do {
184 1         302 return;
185             };
186 7         52 return $pem_encoded_public_key_string;
187             }
188              
189             sub _check_for_der_encoded_private_key {
190 10     10   46 my ( $handle, $file_header, $path ) = @_;
191 10         23 my $pem_encoded_public_key_string;
192 10 50       143 seek $handle, 0, Fcntl::SEEK_SET()
193             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
194 10 50       52 defined read $handle, my $der_encoded_private_key_string,
195             _MAX_PUBLIC_KEY_SIZE()
196             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
197 10         150 my $pem_encoded_private_key_string =
198             "-----BEGIN RSA PRIVATE KEY-----\n"
199             . MIME::Base64::encode_base64($der_encoded_private_key_string)
200             . "-----END RSA PRIVATE KEY-----\n";
201             eval {
202 10         701 my $privkey =
203             Crypt::OpenSSL::RSA->new_private_key($pem_encoded_private_key_string);
204 0         0 $pem_encoded_public_key_string = $privkey->get_public_key_x509_string();
205 10 50       38 } or do {
206 10         182 return;
207             };
208 0         0 return $pem_encoded_public_key_string;
209             }
210              
211             sub _process_pem_private_key {
212 0     0   0 my ( $handle, $file_header, $path ) = @_;
213 0         0 my $pem_encoded_public_key_string;
214 0 0       0 seek $handle, 0, Fcntl::SEEK_SET()
215             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
216 0 0       0 defined read $handle, my $rsa_private_key_string, _MAX_PUBLIC_KEY_SIZE()
217             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
218 0         0 my $privkey = Crypt::OpenSSL::RSA->new_private_key($rsa_private_key_string);
219 0         0 $pem_encoded_public_key_string = $privkey->get_public_key_x509_string();
220 0         0 return $pem_encoded_public_key_string;
221             }
222              
223             sub _process_pem_public_key {
224 8     8   42 my ( $handle, $file_header, $path ) = @_;
225 8         24 my $pem_encoded_public_key_string;
226 8 100       111 if ( $file_header =~ /^[-]{5}BEGIN[ ]RSA[ ]PUBLIC[ ]KEY[-]{5}/smx ) {
227 1 50       38 seek $handle, 0, Fcntl::SEEK_SET()
228             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
229 1 50       13 defined read $handle, my $pem_encoded_rsa_public_key_string,
230             _MAX_PUBLIC_KEY_SIZE()
231             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
232 1         149 my $pubkey = Crypt::OpenSSL::RSA->new_public_key(
233             $pem_encoded_rsa_public_key_string);
234 1         153 $pem_encoded_public_key_string = $pubkey->get_public_key_x509_string();
235             }
236             else {
237 7 50       144 seek $handle, 0, Fcntl::SEEK_SET()
238             or Carp::croak("Failed to seek to start of $path:$EXTENDED_OS_ERROR");
239 7 50       45 defined read $handle, $pem_encoded_public_key_string,
240             _MAX_PUBLIC_KEY_SIZE()
241             or Carp::croak("Failed to read from $path:$EXTENDED_OS_ERROR");
242             }
243 8         111 return $pem_encoded_public_key_string;
244             }
245              
246             sub _get_pem_encoded_public_key_string {
247 10     10   52 my ($x509) = @_;
248 10         27 my $pem_encoded_public_key_string;
249 10 100       224 if ( $x509->key_alg_name() eq 'rsaEncryption' ) {
250 4         631 my $pubkey = Crypt::OpenSSL::RSA->new_public_key( $x509->pubkey() );
251 4         1910 $pem_encoded_public_key_string = $pubkey->get_public_key_x509_string();
252             }
253             else {
254 6         727 $pem_encoded_public_key_string = $x509->pubkey();
255             }
256 10         320 return $pem_encoded_public_key_string;
257             }
258              
259             1; # End of HTTP::PublicKeyPins
260             __END__