File Coverage

blib/lib/Crypt/LE.pm
Criterion Covered Total %
statement 428 837 51.1
branch 175 536 32.6
condition 60 349 17.1
subroutine 63 93 67.7
pod 46 46 100.0
total 772 1861 41.4


line stmt bran cond sub pod time code
1             package Crypt::LE;
2              
3 3     3   332629 use 5.006;
  3         33  
4 3     3   21 use strict;
  3         5  
  3         62  
5 3     3   17 use warnings;
  3         5  
  3         174  
6              
7             our $VERSION = '0.38';
8              
9             =head1 NAME
10              
11             Crypt::LE - Let's Encrypt API interfacing module and client.
12              
13             =head1 VERSION
14              
15             Version 0.38
16              
17             =head1 SYNOPSIS
18              
19             use Crypt::LE;
20            
21             my $le = Crypt::LE->new();
22             $le->load_account_key('account.pem');
23             $le->load_csr('domain.csr');
24             $le->register();
25             $le->accept_tos();
26             $le->request_challenge();
27             $le->accept_challenge(\&process_challenge);
28             $le->verify_challenge();
29             $le->request_certificate();
30             my $cert = $le->certificate();
31             ...
32             sub process_challenge {
33             my $challenge = shift;
34             print "Challenge for $challenge->{domain} requires:\n";
35             print "A file '/.well-known/acme-challenge/$challenge->{token}' with the text: $challenge->{token}.$challenge->{fingerprint}\n";
36             print "When done, press <Enter>";
37             <STDIN>;
38             return 1;
39             };
40              
41             =head1 DESCRIPTION
42              
43             Crypt::LE provides the functionality necessary to use Let's Encrypt API and generate free SSL certificates for your domains. It can also
44             be used to generate RSA keys and Certificate Signing Requests or to revoke previously issued certificates. Crypt::LE is shipped with a
45             self-sufficient client for obtaining SSL certificates - le.pl.
46              
47             B<Provided client supports 'http' and 'dns' domain verification out of the box.>
48              
49             Crypt::LE can be easily extended with custom plugins to handle Let's Encrypt challenges. See L<Crypt::LE::Challenge::Simple> module
50             for an example of a challenge-handling plugin.
51              
52             Basic usage:
53              
54             B<le.pl --key account.key --csr domain.csr --csr-key domain.key --crt domain.crt --domains "www.domain.ext,domain.ext" --generate-missing>
55              
56             That will generate an account key and a CSR (plus key) if they are missing. If any of those files exist, they will just be loaded, so it is safe to re-run
57             the client. Run le.pl without any parameters or with C<--help> to see more details and usage examples.
58              
59             In addition to challenge-handling plugins, the client also supports completion-handling plugins, such as L<Crypt::LE::Complete::Simple>. You can easily
60             handle challenges and trigger specific actions when your certificate gets issued by using those modules as templates, without modifying the client code.
61             You can also pass custom parameters to your modules from le.pl command line:
62              
63             B<le.pl ... --handle-with Crypt::LE::Challenge::Simple --handle-params '{"key1": 1, "key2": "one"}'>
64            
65             B<le.pl ... --complete-with Crypt::LE::Complete::Simple --complete-params '{"key1": 1, "key2": "one"}'>
66            
67             The parameters don't have to be put directly in the command line, you could also give a name of a file containing valid JSON to read them from.
68              
69             B<le.pl ... --complete-params complete.json>
70            
71             Crypt::LE::Challenge:: and Crypt::LE::Complete:: namespaces are suggested for new plugins.
72              
73             =head1 EXPORT
74              
75             Crypt::LE does not export anything by default, but allows you to import the following constants:
76              
77             =over
78              
79             =item *
80             OK
81              
82             =item *
83             READ_ERROR
84              
85             =item *
86             LOAD_ERROR
87              
88             =item *
89             INVALID_DATA
90              
91             =item *
92             DATA_MISMATCH
93              
94             =item *
95             UNSUPPORTED
96              
97             =item *
98             ALREADY_DONE
99              
100             =item *
101             BAD_REQUEST
102              
103             =item *
104             AUTH_ERROR
105              
106             =item *
107             ERROR
108              
109             =back
110              
111             To import all of those, use C<':errors'> tag:
112              
113             use Crypt::LE ':errors';
114             ...
115             $le->load_account_key('account.pem') == OK or die "Could not load the account key: " . $le->error_details;
116            
117             If you don't want to use error codes while checking whether the last called method has failed or not, you can use the
118             rule of thumb that on success it will return zero. You can also call error() or error_details() methods, which
119             will be set with some values on error.
120              
121             =cut
122              
123 3     3   1671 use Crypt::OpenSSL::RSA;
  3         23312  
  3         113  
124 3     3   1553 use JSON::MaybeXS;
  3         22194  
  3         192  
125 3     3   2234 use HTTP::Tiny;
  3         127203  
  3         141  
126 3     3   1654 use IO::File;
  3         4851  
  3         362  
127 3     3   1800 use Digest::SHA 'sha256';
  3         9602  
  3         269  
128 3     3   1046 use MIME::Base64 qw<encode_base64url decode_base64url decode_base64 encode_base64>;
  3         1301  
  3         260  
129 3     3   2094 use Net::SSLeay qw<XN_FLAG_RFC2253 ASN1_STRFLGS_ESC_MSB MBSTRING_UTF8>;
  3         15871  
  3         1448  
130 3     3   28 use Scalar::Util 'blessed';
  3         7  
  3         181  
131 3     3   1761 use Encode 'encode_utf8';
  3         46161  
  3         251  
132 3     3   24 use Storable 'dclone';
  3         6  
  3         147  
133 3     3   1508 use Convert::ASN1;
  3         71575  
  3         163  
134 3     3   1629 use Module::Load;
  3         3423  
  3         21  
135 3     3   1820 use Time::Piece;
  3         29713  
  3         19  
136 3     3   233 use Time::Seconds;
  3         7  
  3         185  
137 3     3   1973 use Data::Dumper;
  3         19272  
  3         282  
138 3     3   24 use base 'Exporter';
  3         7  
  3         542  
139              
140             Net::SSLeay::randomize();
141             Net::SSLeay::load_error_strings();
142             Net::SSLeay::ERR_load_crypto_strings();
143             Net::SSLeay::OpenSSL_add_ssl_algorithms();
144             Net::SSLeay::OpenSSL_add_all_digests();
145             our $keysize = 4096;
146             our $keycurve = 'prime256v1';
147             our $headers = { 'Content-type' => 'application/jose+json' };
148              
149             use constant {
150 3         38789 OK => 0,
151             READ_ERROR => 1,
152             LOAD_ERROR => 2,
153             INVALID_DATA => 3,
154             DATA_MISMATCH => 4,
155             UNSUPPORTED => 5,
156             ERROR => 500,
157              
158             SUCCESS => 200,
159             CREATED => 201,
160             ACCEPTED => 202,
161             BAD_REQUEST => 400,
162             AUTH_ERROR => 403,
163             ALREADY_DONE => 409,
164              
165             KEY_RSA => 0,
166             KEY_ECC => 1,
167              
168             PEER_CRT => 4,
169             CRT_DEPTH => 5,
170              
171             SAN => '2.5.29.17',
172 3     3   20 };
  3         6  
173              
174             our @EXPORT_OK = (qw<OK READ_ERROR LOAD_ERROR INVALID_DATA DATA_MISMATCH UNSUPPORTED ERROR BAD_REQUEST AUTH_ERROR ALREADY_DONE KEY_RSA KEY_ECC>);
175             our %EXPORT_TAGS = ( 'errors' => [ @EXPORT_OK[0..9] ], 'keys' => [ @EXPORT_OK[10..11] ] );
176              
177             my $pkcs12_available = 0;
178             my $j = JSON->new->canonical()->allow_nonref();
179             my $url_safe = qr/^[-_A-Za-z0-9]+$/; # RFC 4648 section 5.
180             my $flag_rfc22536_utf8 = (XN_FLAG_RFC2253) & (~ ASN1_STRFLGS_ESC_MSB);
181             if ($^O eq 'MSWin32') {
182             eval { autoload 'Crypt::OpenSSL::PKCS12'; };
183             $pkcs12_available = 1 unless $@;
184             }
185              
186             # https://github.com/letsencrypt/boulder/blob/master/core/good_key.go
187             my @primes = map { Crypt::OpenSSL::Bignum->new_from_decimal($_) } (
188             2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47,
189             53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107,
190             109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167,
191             173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229,
192             233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283,
193             293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359,
194             367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431,
195             433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491,
196             499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571,
197             577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641,
198             643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709,
199             719, 727, 733, 739, 743, 751
200             );
201              
202             my $asn = Convert::ASN1->new();
203             $asn->prepare(q<
204             Extensions ::= SEQUENCE OF Extension
205             Extension ::= SEQUENCE {
206             extnID OBJECT IDENTIFIER,
207             critical BOOLEAN OPTIONAL,
208             extnValue OCTET STRING
209             }
210             SubjectAltName ::= GeneralNames
211             GeneralNames ::= SEQUENCE OF GeneralName
212             GeneralName ::= CHOICE {
213             otherName [0] ANY,
214             rfc822Name [1] IA5String,
215             dNSName [2] IA5String,
216             x400Address [3] ANY,
217             directoryName [4] ANY,
218             ediPartyName [5] ANY,
219             uniformResourceIdentifier [6] IA5String,
220             iPAddress [7] OCTET STRING,
221             registeredID [8] OBJECT IDENTIFIER
222             }
223             >);
224              
225             my $compat = {
226             newAccount => 'new-reg',
227             newOrder => 'new-cert',
228             revokeCert => 'revoke-cert',
229             };
230              
231             =head1 METHODS (API Setup)
232              
233             The following methods are provided for the API setup. Please note that account key setup by default requests the resource directory from Let's Encrypt servers.
234             This can be changed by resetting the 'autodir' parameter of the constructor.
235              
236             =head2 new()
237              
238             Create a new instance of the class. Initialize the object with passed parameters. Normally you don't need to use any, but the following are supported:
239              
240             =over 12
241              
242             =item C<ua>
243              
244             User-agent name to use while sending requests to Let's Encrypt servers. By default set to module name and version.
245              
246             =item C<server>
247              
248             Server URL to connect to. Only needed if the default live or staging server URLs have changed and this module has not yet been updated with the new
249             information or if you are using a custom server supporting ACME protocol. Note: the value is supposed to point to the root of the API (for example:
250             https://some.server/acme/) rather than the directory handler. This parameter might be deprecated in the future in favour of the 'dir' one below.
251              
252             =item C<live>
253              
254             Set to true to connect to a live Let's Encrypt server. By default it is not set, so staging server is used, where you can test the whole process of getting
255             SSL certificates.
256              
257             =item C<debug>
258              
259             Activates printing debug messages to the standard output when set. If set to 1, only standard messages are printed. If set to any greater value, then structures and
260             server responses are printed as well.
261              
262             =item C<dir>
263              
264             Full URL of a 'directory' handler on the server (the actual name of the handler can be different in certain configurations, where multiple handlers
265             are mapped). Only needed if you are using a custom server supporting ACME protocol. This parameter replaces the 'server' one.
266              
267             =item C<autodir>
268              
269             Enables automatic retrieval of the resource directory (required for normal API processing) from the servers. Enabled by default.
270              
271             =item C<delay>
272              
273             Specifies the time in seconds to wait before Let's Encrypt servers are checked for the challenge verification results again. By default set to 2 seconds.
274             Non-integer values are supported (so for example you can set it to 1.5 if you like).
275              
276             =item C<version>
277              
278             Enforces the API version to be used. If the response is not found to be compatible, an error will be returned. If not set, system will try to make an educated guess.
279              
280             =item C<try>
281              
282             Specifies the amount of retries to attempt while in 'pending' state and waiting for verification results response. By default set to 300, which combined
283             with the delay of 2 seconds gives you 10 minutes of waiting.
284              
285             =item C<logger>
286              
287             Logger instance to use for debug messages. If not given, the messages will be printed to STDOUT.
288              
289             =back
290              
291             Returns: L<Crypt::LE> object.
292              
293             =cut
294              
295             sub new {
296 2     2 1 2422 my $class = shift;
297 2         8 my %params = @_;
298 2         18 my $self = {
299             ua => '',
300             server => '',
301             dir => '',
302             live => 0,
303             debug => 0,
304             autodir => 1,
305             delay => 2,
306             version => 0,
307             try => 300,
308             };
309 2         5 foreach my $key (keys %{$self}) {
  2         8  
310 18 100 66     89 $self->{$key} = $params{$key} if (exists $params{$key} and !ref $params{$key});
311             }
312             # Init UA
313 2   33     31 $self->{ua} = HTTP::Tiny->new( agent => $self->{ua} || __PACKAGE__ . " v$VERSION", verify_SSL => 1 );
314             # Init server
315 2 50       201 if ($self->{server}) {
316             # Custom server - drop the protocol if given (defaults to https later). If that leaves nothing, the check below
317             # will set the servers to LE standard ones.
318 0         0 $self->{server}=~s~^\w+://~~;
319             }
320 2 50       9 if ($self->{dir}) {
321 0 0       0 $self->{dir} = "https://$self->{dir}" unless $self->{dir}=~m~^https?://~i;
322             }
323 2 50       10 unless ($self->{server}) {
324 2 50       8 $self->{server} = $self->{live} ? 'acme-v02.api.letsencrypt.org' : 'acme-staging-v02.api.letsencrypt.org';
325             }
326             # Init logger
327 2 50 33     9 $self->{logger} = $params{logger} if ($params{logger} and blessed $params{logger});
328 2         7 bless $self, $class;
329             }
330              
331             #====================================================================================================
332             # API Setup functions
333             #====================================================================================================
334              
335             =head2 load_account_key($filename|$scalar_ref)
336              
337             Loads the private account key from the file or scalar in PEM or DER formats.
338              
339             Returns: OK | READ_ERROR | LOAD_ERROR | INVALID_DATA.
340              
341             =cut
342              
343             sub load_account_key {
344 4     4 1 766 my ($self, $file) = @_;
345 4         14 $self->_reset_key;
346 4         14 my $key = $self->_file($file);
347 4 100       15 return $self->_status(READ_ERROR, "Key reading error.") unless $key;
348 3         6 eval {
349 3         10 $key = Crypt::OpenSSL::RSA->new_private_key($self->_convert($key, 'RSA PRIVATE KEY'));
350             };
351 3 100       15 return $self->_status(LOAD_ERROR, "Key loading error.") if $@;
352 2         6 return $self->_set_key($key, "Account key loaded.");
353             }
354              
355             =head2 generate_account_key()
356              
357             Generates a new private account key of the $keysize bits (4096 by default). The key is additionally validated for not being divisible by small primes.
358              
359             Returns: OK | INVALID_DATA.
360              
361             =cut
362              
363             sub generate_account_key {
364 1     1 1 3 my $self = shift;
365 1         3 my ($pk, $err, $code) = _key();
366 1 50 0     5 return $self->_status(INVALID_DATA, $err||"Could not generate account key") unless $pk;
367 1         242 my $key = Crypt::OpenSSL::RSA->new_private_key(Net::SSLeay::PEM_get_string_PrivateKey($pk));
368 1         10 _free(k => $pk);
369 1         7 return $self->_set_key($key, "Account key generated.");
370             }
371              
372             =head2 account_key()
373              
374             Returns: A previously loaded or generated private key in PEM format or undef.
375              
376             =cut
377              
378             sub account_key {
379 3     3 1 545 return shift->{pem};
380             }
381              
382             =head2 load_csr($filename|$scalar_ref [, $domains])
383              
384             Loads Certificate Signing Requests from the file or scalar. Domains list can be omitted or it can be given as a string of comma-separated names or as an array reference.
385             If omitted, then names will be loaded from the CSR. If it is given, then the list of names will be verified against those found on CSR.
386              
387             Returns: OK | READ_ERROR | LOAD_ERROR | INVALID_DATA | DATA_MISMATCH.
388              
389             =cut
390              
391             sub load_csr {
392 11     11 1 590 my $self = shift;
393 11         24 my ($file, $domains) = @_;
394 11         29 $self->_reset_csr;
395 11         31 my $csr = $self->_file($file);
396 11 100       31 return $self->_status(READ_ERROR, "CSR reading error.") unless $csr;
397 10         49 my $bio = Net::SSLeay::BIO_new(Net::SSLeay::BIO_s_mem());
398 10 50       20 return $self->_status(LOAD_ERROR, "Could not allocate memory for the CSR") unless $bio;
399 10         18 my ($in, $cn, $san, $i);
400 10 100 66     571 unless (Net::SSLeay::BIO_write($bio, $csr) and $in = Net::SSLeay::PEM_read_bio_X509_REQ($bio)) {
401 1         4 _free(b => $bio);
402 1         4 return $self->_status(LOAD_ERROR, "Could not load the CSR");
403             }
404 9         28 $cn = Net::SSLeay::X509_REQ_get_subject_name($in);
405 9 50       19 if ($cn) {
406 9         110 $cn = Net::SSLeay::X509_NAME_print_ex($cn, $flag_rfc22536_utf8, 1);
407 9 50 33     110 $cn = lc($1) if ($cn and $cn=~/^.*?\bCN=([^\s,]+).*$/);
408             }
409 9         14 my @list = @{$self->_get_list($domains)};
  9         26  
410 9         227 $i = Net::SSLeay::X509_REQ_get_attr_by_NID($in, &Net::SSLeay::NID_ext_req, -1);
411 9 50       182 if ($i > -1) {
412 9         30 my $o = Net::SSLeay::P_X509_REQ_get_attr($in, $i);
413 9 50       17 if ($o) {
414 9         36 my $exts = $asn->find("Extensions");
415 9         176 my $dec = $exts->decode(Net::SSLeay::P_ASN1_STRING_get($o));
416 9 50       2651 if ($dec) {
417 9         12 foreach my $ext (@{$dec}) {
  9         18  
418 9 50 33     39 if ($ext->{extnID} and $ext->{extnID} eq SAN) {
419 9         26 $exts = $asn->find("SubjectAltName");
420 9         136 $san = $exts->decode($ext->{extnValue});
421 9         1664 last;
422             }
423             }
424             }
425             }
426             }
427 9         22 my @loaded_domains = ();
428 9         14 my %seen = ();
429 9         11 my $san_broken;
430 9 50       21 if ($cn) {
431 9         18 push @loaded_domains, $cn;
432 9         19 $seen{$cn} = 1;
433             }
434 9 50       24 if ($san) {
435 9         11 foreach my $ext (@{$san}) {
  9         16  
436 21 50       32 if ($ext->{dNSName}) {
437 21         41 $cn = lc($ext->{dNSName});
438 21 100       58 push @loaded_domains, $cn unless $seen{$cn}++;
439             } else {
440 0         0 $san_broken++;
441             }
442             }
443             }
444 9         24 _free(b => $bio);
445 9 50       17 if ($san_broken) {
446 0         0 return $self->_status(INVALID_DATA, "CSR contains $san_broken non-DNS record(s) in SAN");
447             }
448 9 50       20 unless (@loaded_domains) {
449 0         0 return $self->_status(INVALID_DATA, "No domains found on CSR.");
450             } else {
451 9 100       22 if (my $odd = $self->_verify_list(\@loaded_domains)) {
452 1         4 return $self->_status(INVALID_DATA, "Unsupported domain names on CSR: " . join(", ", @{$odd}));
  1         6  
453             }
454 8         49 $self->_debug("Loaded domain names from CSR: " . join(', ', @loaded_domains));
455             }
456 8 100       19 if (@list) {
457 4 100       30 return $self->_status(DATA_MISMATCH, "The list of provided domains does not match the one on the CSR.") unless (join(',', sort @loaded_domains) eq join(',', sort @list));
458 2         5 @loaded_domains = @list; # Use the command line domain order if those were listed along with CSR.
459             }
460 6         28 $self->_set_csr($csr, undef, \@loaded_domains);
461 6         15 return $self->_status(OK, "CSR loaded.");
462             }
463              
464             =head2 generate_csr($domains, [$key_type], [$key_attr])
465              
466             Generates a new Certificate Signing Request. Optionally accepts key type and key attribute parameters, where key type should
467             be either KEY_RSA or KEY_ECC (if supported on your system) and key attribute is either the key size (for RSA) or the curve (for ECC).
468             By default an RSA key of 4096 bits will be used.
469             Domains list is mandatory and can be given as a string of comma-separated names or as an array reference.
470              
471             Returns: OK | ERROR | UNSUPPORTED | INVALID_DATA.
472              
473             =cut
474              
475             sub generate_csr {
476 14     14 1 182 my $self = shift;
477 14         30 my ($domains, $key_type, $key_attr) = @_;
478 14         41 $self->_reset_csr;
479 14         23 my @list = @{$self->_get_list($domains)};
  14         33  
480 14 100       42 return $self->_status(INVALID_DATA, "No domains provided.") unless @list;
481 13 100       38 if (my $odd = $self->_verify_list(\@list)) {
482 4         9 return $self->_status(INVALID_DATA, "Unsupported domain names provided: " . join(", ", @{$odd}));
  4         18  
483             }
484 9         25 my ($key, $err, $code) = _key($self->csr_key(), $key_type, $key_attr);
485 9 100 100     37 return $self->_status($code||ERROR, $err||"Key problem while creating CSR") unless $key;
      50        
486 6         47 my ($csr, $csr_key) = _csr($key, \@list, { O => '-', L => '-', ST => '-', C => 'GB' });
487 6 50       27 return $self->_status(ERROR, "Unexpected CSR error.") unless $csr;
488 6         30 $self->_set_csr($csr, $csr_key, \@list);
489 6         18 return $self->_status(OK, "CSR generated.");
490             }
491              
492             =head2 csr()
493              
494             Returns: A previously loaded or generated CSR in PEM format or undef.
495              
496             =cut
497              
498             sub csr {
499 3     3 1 14 return shift->{csr};
500             }
501              
502             =head2 load_csr_key($filename|$scalar_ref)
503              
504             Loads the CSR key from the file or scalar (to be used for generating a new CSR).
505              
506             Returns: OK | READ_ERROR.
507              
508             =cut
509              
510             sub load_csr_key {
511 3     3 1 687 my $self = shift;
512 3         6 my $file = shift;
513 3         9 undef $self->{csr_key};
514 3         10 my $key = $self->_file($file);
515 3 100       14 return $self->_status(READ_ERROR, "CSR key reading error.") unless $key;
516 1         2 $self->{csr_key} = $key;
517 1         4 return $self->_status(OK, "CSR key loaded");
518             }
519              
520             =head2 csr_key()
521              
522             Returns: A CSR key (either loaded or generated with CSR) or undef.
523              
524             =cut
525              
526             sub csr_key {
527 10     10 1 32 return shift->{csr_key};
528             }
529              
530             =head2 set_account_email([$email])
531              
532             Sets (or resets if no parameter is given) an email address that will be used for registration requests.
533              
534             Returns: OK | INVALID_DATA.
535              
536             =cut
537              
538             sub set_account_email {
539 0     0 1 0 my ($self, $email) = @_;
540 0 0       0 unless ($email) {
541 0         0 undef $self->{email};
542 0         0 return $self->_status(OK, "Account email has been reset");
543             }
544             # Note: We don't validate email, just removing some extra bits which may be present.
545 0         0 $email=~s/^\s*mail(?:to):\s*//i;
546 0         0 $email=~s/^<([^>]+)>/$1/;
547 0         0 $email=~s/^\s+$//;
548 0 0       0 return $self->_status(INVALID_DATA, "Invalid email provided") unless $email;
549 0         0 $self->{email} = $email;
550 0         0 return $self->_status(OK, "Account email has been set to '$email'");
551             }
552              
553             =head2 set_domains($domains)
554              
555             Sets the list of domains to be used for verification process. This call is optional if you load or generate a CSR, in which case the list of the domains will be set at that point.
556              
557             Returns: OK | INVALID_DATA.
558              
559             =cut
560              
561             sub set_domains {
562 9     9 1 5362 my ($self, $domains) = @_;
563 9         18 my @list = @{$self->_get_list($domains)};
  9         30  
564 9 100       32 return $self->_status(INVALID_DATA, "No domains provided.") unless @list;
565 8 100       22 if (my $odd = $self->_verify_list(\@list)) {
566 4         8 return $self->_status(INVALID_DATA, "Unsupported domain names provided: " . join(", ", @{$odd}));
  4         17  
567             }
568 4         12 $self->{loaded_domains} = \@list;
569 4         7 my %loaded_domains = map {$_, undef} @list;
  9         26  
570 4         12 $self->{domains} = \%loaded_domains;
571 4         15 return $self->_status(OK, "Domains list is set");
572             }
573              
574             =head2 set_version($version)
575              
576             Sets the API version to be used. To pick the version automatically, use 0, other accepted values are currently 1 and 2.
577              
578             Returns: OK | INVALID_DATA.
579              
580             =cut
581              
582             sub set_version {
583 1     1 1 3 my ($self, $version) = @_;
584 1 50 33     15 return $self->_status(INVALID_DATA, "Unsupported API version") unless (defined $version and $version=~/^\d+$/ and $version <= 2);
      33        
585 1         6 $self->{version} = $version;
586 1         8 return $self->_status(OK, "API version is set to $version.");
587             }
588              
589             =head2 version()
590              
591             Returns: The API version currently used (1 or 2). If 0 is returned, it means it is set to automatic detection and the directory has not yet been retrieved.
592              
593             =cut
594              
595             sub version {
596 1     1 1 3 my $self = shift;
597 1         5 return $self->{version};
598             }
599              
600             #====================================================================================================
601             # API Setup helpers
602             #====================================================================================================
603              
604             sub _reset_key {
605 4     4   8 my $self = shift;
606 4         37 undef $self->{$_} for qw<key_params key pem jwk fingerprint>;
607             }
608              
609             sub _set_key {
610 3     3   20 my $self = shift;
611 3         9 my ($key, $msg) = @_;
612 3         130 my $pem = $key->get_private_key_string;
613 3         87 my ($n, $e) = $key->get_key_parameters;
614 3 50       1628 return $self->_status(INVALID_DATA, "Key modulus is divisible by a small prime and will be rejected.") if $self->_is_divisible($n);
615 3         13 $key->use_pkcs1_padding;
616 3         9 $key->use_sha256_hash;
617 3         13 $self->{key_params} = { n => $n, e => $e };
618 3         8 $self->{key} = $key;
619 3         7 $self->{pem} = $pem;
620 3         9 $self->{jwk} = $self->_jwk();
621 3         117 $self->{fingerprint} = encode_base64url(sha256($j->encode($self->{jwk})));
622 3 50       41 if ($self->{autodir}) {
623 0         0 my $status = $self->directory;
624 0 0       0 return $status unless ($status == OK);
625             }
626 3         11 return $self->_status(OK, $msg);
627             }
628              
629             sub _is_divisible {
630 3     3   8 my ($self, $n) = @_;
631 3         5 my ($quotient, $remainder);
632 3         18 my $ctx = Crypt::OpenSSL::Bignum::CTX->new();
633 3         9 foreach my $prime (@primes) {
634 399         3040 ($quotient, $remainder) = $n->div($prime, $ctx);
635 399 50       1294 return 1 if $remainder->is_zero;
636             }
637 3         21 return 0;
638             }
639              
640             sub _reset_csr {
641 25     25   36 my $self = shift;
642 25         141 undef $self->{$_} for qw<domains loaded_domains csr>;
643             }
644              
645             sub _set_csr {
646 12     12   24 my $self = shift;
647 12         29 my ($csr, $pk, $domains) = @_;
648 12         49 $self->{csr} = $csr;
649 12         29 $self->{csr_key} = $pk;
650 12         18 my %loaded_domains = map {$_, undef} @{$domains};
  22         70  
  12         26  
651 12         20 $self->{loaded_domains} = $domains;
652 12         31 $self->{domains} = \%loaded_domains;
653             }
654              
655             sub _get_list {
656 32     32   61 my ($self, $list) = @_;
657 32 100       191 return [ map {lc $_} (ref $list eq 'ARRAY') ? @{$list} : $list ? split /\s*,\s*/, $list : () ];
  40 100       175  
  1         3  
658             }
659              
660             sub _verify_list {
661 30     30   55 my ($self, $list) = @_;
662 30 100 100     47 my @odd = grep { /[\s\[\{\(\<\@\>\)\}\]\/\\:]/ or /^[\d\.]+$/ or !/\./ } @{$list};
  49         407  
  30         57  
663 30 100       141 return @odd ? \@odd : undef;
664             }
665              
666             #====================================================================================================
667             # API Workflow functions
668             #====================================================================================================
669              
670             =head1 METHODS (API Workflow)
671              
672             The following methods are provided for the API workflow processing. All but C<accept_challenge()> methods interact with Let's Encrypt servers.
673              
674             =head2 directory([ $reload ])
675              
676             Loads resource pointers from Let's Encrypt. This method needs to be called before the registration. It
677             will be called automatically upon account key loading/generation unless you have reset the 'autodir'
678             parameter when creating a new Crypt::LE instance. If any true value is provided as a parameter, reloads
679             the directory even if it has been already retrieved, but preserves the 'reg' value (for example to pull
680             another Nonce for the current session).
681              
682             Returns: OK | INVALID_DATA | LOAD_ERROR.
683              
684             =cut
685              
686             sub directory {
687 1     1 1 3707 my ($self, $reload) = @_;
688 1 50 33     9 if (!$self->{directory} or $reload) {
689 1 50       14 my ($status, $content) = $self->{dir} ? $self->_request($self->{dir}) : $self->_request("https://$self->{server}/directory");
690 1 50 33     17 if ($status == SUCCESS and $content and (ref $content eq 'HASH')) {
      33        
691 1 50       4 if ($content->{newAccount}) {
    0          
692 1 50 0     7 unless ($self->version) {
693 1         5 $self->set_version(2);
694             } elsif ($self->version() != 2) {
695             return $self->_status(INVALID_DATA, "Resource directory is not compatible with the version set (required v1, got v2).");
696             }
697 1         5 $self->_compat($content);
698             } elsif ($content->{'new-reg'}) {
699 0 0 0     0 unless ($self->version) {
700 0         0 $self->set_version(1);
701             } elsif ($self->version() != 1) {
702             return $self->_status(INVALID_DATA, "Resource directory is not compatible with the version set (required v2, got v1).");
703             }
704             } else {
705 0         0 return $self->_status(INVALID_DATA, "Resource directory does not contain expected fields.");
706             }
707 1 0 33     6 $content->{reg} = $self->{directory}->{reg} if ($self->{directory} and $self->{directory}->{reg});
708 1         4 $self->{directory} = $content;
709 1 50       4 unless ($self->{nonce}) {
710 1 50       3 if ($self->{directory}->{'newNonce'}) {
711 1         9 $self->_request($self->{directory}->{'newNonce'}, undef, { method => 'head' });
712 1 50       6 return $self->_status(LOAD_ERROR, "Could not retrieve the Nonce value.") unless $self->{nonce};
713             } else {
714 0         0 return $self->_status(LOAD_ERROR, "Could not retrieve the Nonce value and there is no method to request it.")
715             }
716             }
717 1         5 return $self->_status(OK, "Directory loaded successfully.");
718             } else {
719 0         0 return $self->_status(LOAD_ERROR, $content);
720             }
721             }
722 0         0 return $self->_status(OK, "Directory has been already loaded.");
723             }
724              
725             =head2 new_nonce()
726              
727             Requests a new nonce by forcing the directory reload. Picks up the value from the returned headers if it
728             is present (API v1.0), otherwise uses newNonce method to get it (API v2.0) if one is provided.
729              
730             Returns: Nonce value or undef (if neither the value is in the headers nor newNonce method is available).
731              
732             =cut
733              
734             sub new_nonce {
735 0     0 1 0 my $self = shift;
736 0         0 undef $self->{nonce};
737 0         0 $self->directory(1);
738 0         0 return $self->{nonce};
739             }
740              
741             =head2 register()
742              
743             Registers an account key with Let's Encrypt. If the key is already registered, it will be handled automatically.
744              
745             Returns: OK | ERROR.
746              
747             =cut
748              
749             sub register {
750 0     0 1 0 my $self = shift;
751 0         0 my $req = { resource => 'new-reg' };
752 0 0       0 $req->{contact} = [ "mailto:$self->{email}" ] if $self->{email};
753 0         0 my ($status, $content) = $self->_request($self->{directory}->{'new-reg'}, $req);
754 0 0       0 $self->{directory}->{reg} = $self->{location} if $self->{location};
755 0         0 $self->{$_} = undef for (qw<registration_id contact_details>);
756 0 0       0 if ($status == $self->_compat_response(ALREADY_DONE)) {
    0          
757 0         0 $self->{new_registration} = 0;
758 0         0 $self->_debug("Key is already registered, reg path: $self->{directory}->{reg}.");
759 0         0 ($status, $content) = $self->_request($self->{directory}->{'reg'}, { resource => 'reg' });
760 0 0       0 if ($status == $self->_compat_response(ACCEPTED)) {
761 0         0 $self->{registration_info} = $content;
762 0 0 0     0 if ($self->version() == 1 and $self->{links} and $self->{links}->{'terms-of-service'} and (!$content->{agreement} or ($self->{links}->{'terms-of-service'} ne $content->{agreement}))) {
      0        
      0        
      0        
763 0 0       0 $self->_debug($content->{agreement} ? "You need to accept TOS" : "TOS has changed, you may need to accept it again.");
764 0         0 $self->{tos_changed} = 1;
765             } else {
766 0         0 $self->{tos_changed} = 0;
767             }
768             } else {
769 0         0 return $self->_status(ERROR, $content);
770             }
771             } elsif ($status == CREATED) {
772 0         0 $self->{new_registration} = 1;
773 0         0 $self->{registration_info} = $content;
774 0         0 $self->{tos_changed} = 0;
775 0         0 my $tos_message = '';
776 0 0       0 if ($self->{links}->{'terms-of-service'}) {
777 0         0 $self->{tos_changed} = 1;
778 0         0 $tos_message = "You need to accept TOS at $self->{links}->{'terms-of-service'}";
779             }
780 0         0 $self->_debug("New key is now registered, reg path: $self->{directory}->{reg}. $tos_message");
781             } else {
782 0         0 return $self->_status(ERROR, $content);
783             }
784 0 0 0     0 if ($self->{registration_info} and ref $self->{registration_info} eq 'HASH') {
785 0         0 $self->{registration_id} = $self->{registration_info}->{id};
786 0 0 0     0 if ($self->{registration_info}->{contact} and (ref $self->{registration_info}->{contact} eq 'ARRAY') and @{$self->{registration_info}->{contact}}) {
  0   0     0  
787 0         0 $self->{contact_details} = $self->{registration_info}->{contact};
788             }
789             }
790 0 0 0     0 if (!$self->{registration_id} and $self->{directory}->{reg}=~/\/([^\/]+)$/) {
791 0         0 $self->{registration_id} = $1;
792             }
793 0 0       0 $self->_debug("Account ID: $self->{registration_id}") if $self->{registration_id};
794 0         0 return $self->_status(OK, "Registration success: TOS change status - $self->{tos_changed}, new registration flag - $self->{new_registration}.");
795             }
796              
797             =head2 accept_tos()
798              
799             Accepts Terms of Service set by Let's Encrypt.
800              
801             Returns: OK | ERROR.
802              
803             =cut
804              
805             sub accept_tos {
806 0     0 1 0 my $self = shift;
807 0 0       0 return $self->_status(OK, "TOS has NOT been changed, no need to accept again.") unless $self->tos_changed;
808 0         0 my ($status, $content) = $self->_request($self->{directory}->{'reg'}, { resource => 'reg', agreement => $self->{links}->{'terms-of-service'} });
809 0 0       0 return ($status == $self->_compat_response(ACCEPTED)) ? $self->_status(OK, "Accepted TOS.") : $self->_status(ERROR, $content);
810             }
811              
812             =head2 update_contacts($array_ref)
813              
814             Updates contact details for your Let's Encrypt account. Accepts an array reference of contacts.
815             Non-prefixed contacts will be automatically prefixed with 'mailto:'.
816              
817             Returns: OK | INVALID_DATA | ERROR.
818              
819             =cut
820              
821             sub update_contacts {
822 0     0 1 0 my ($self, $contacts) = @_;
823 0 0 0     0 return $self->_status(INVALID_DATA, "Invalid call parameters.") unless ($contacts and (ref $contacts eq 'ARRAY'));
824 0 0       0 my @set = map { /^\w+:/ ? $_ : "mailto:$_" } @{$contacts};
  0         0  
  0         0  
825 0         0 my ($status, $content) = $self->_request($self->{directory}->{'reg'}, { resource => 'reg', contact => \@set });
826 0 0       0 return ($status == $self->_compat_response(ACCEPTED)) ? $self->_status(OK, "Email has been updated.") : $self->_status(ERROR, $content);
827             }
828              
829             =head2 request_challenge()
830              
831             Requests challenges for domains on your CSR. On error you can call failed_domains() method, which returns an array reference to domain names for which
832             the challenge was not requested successfully.
833              
834             Returns: OK | ERROR.
835              
836             =cut
837              
838             sub request_challenge {
839 0     0 1 0 my $self = shift;
840 0 0       0 $self->_status(ERROR, "No domains are set.") unless $self->{domains};
841 0         0 my ($domains_requested, %domains_failed);
842             # For v2.0 API the 'new-authz' is optional. However, authz set is provided via newOrder request (also utilized by request_certificate call).
843             # We are keeping the flow compatible with older clients, so if that call has not been specifically made (as it would in le.pl), we do
844             # it at the point of requesting the challenge. Note that if certificate is already valid, we will skip most of the challenge-related
845             # calls, but will not be returning the cert early to avoid interrupting the established flow.
846 0 0       0 if ($self->version() > 1) {
847 0 0       0 unless ($self->{authz}) {
848 0         0 my ($status, $content) = $self->_request($self->{directory}->{'new-cert'}, { resource => 'new-cert' });
849 0 0 0     0 if ($status == CREATED and $content->{'identifiers'} and $content->{'authorizations'}) {
      0        
850 0         0 push @{$self->{authz}}, [ $_, '' ] for @{$content->{'authorizations'}};
  0         0  
  0         0  
851 0         0 $self->{finalize} = $content->{'finalize'};
852             } else {
853 0 0       0 unless ($self->{directory}->{'new-authz'}) {
854 0         0 return $self->_status(ERROR, "Cannot request challenges - " . $self->_pull_error($content) . "($status).");
855             }
856 0         0 $self->_get_authz();
857             }
858             }
859             } else {
860 0         0 $self->_get_authz();
861             }
862 0         0 foreach my $authz (@{$self->{authz}}) {
  0         0  
863 0         0 $self->_debug("Requesting challenge.");
864 0         0 my ($status, $content) = $self->_request(@{$authz});
  0         0  
865 0         0 $domains_requested++;
866 0 0       0 if ($status == $self->_compat_response(CREATED)) {
867 0         0 my $valid_challenge = 0;
868 0 0 0     0 return $self->_status(ERROR, "Missing identifier in the authz response.") unless ($content->{identifier} and $content->{identifier}->{value});
869 0         0 my $domain = $content->{identifier}->{value};
870 0 0       0 $domain = "*.$domain" if $content->{wildcard};
871 0         0 foreach my $challenge (@{$content->{challenges}}) {
  0         0  
872 0 0 0     0 unless ($challenge and (ref $challenge eq 'HASH') and $challenge->{type} and
      0        
      0        
      0        
      0        
      0        
873             ($challenge->{url} or $challenge->{uri}) and
874             ($challenge->{status} or $content->{status})) {
875 0         0 $self->_debug("Challenge for domain $domain does not contain required fields.");
876 0         0 next;
877             }
878 0         0 my $type = (split '-', delete $challenge->{type})[0];
879 0 0 0     0 unless ($challenge->{token} and $challenge->{token}=~$url_safe) {
880 0         0 $self->_debug("Challenge ($type) for domain $domain is missing a valid token.");
881 0         0 next;
882             }
883 0 0       0 $valid_challenge = 1 if ($challenge->{status} eq 'valid');
884 0   0     0 $challenge->{uri} ||= $challenge->{url};
885 0   0     0 $challenge->{status} ||= $content->{status};
886 0         0 $self->{challenges}->{$domain}->{$type} = $challenge;
887             }
888 0 0 0     0 if ($self->{challenges} and exists $self->{challenges}->{$domain}) {
889 0         0 $self->_debug("Received challenges for $domain.");
890 0         0 $self->{domains}->{$domain} = $valid_challenge;
891             } else {
892 0         0 $self->_debug("Received no valid challenges for $domain.");
893 0   0     0 $domains_failed{$domain} = $self->_pull_error($content)||'No valid challenges';
894             }
895             } else {
896             # NB: In API v2.0 you don't know which domain you are receiving a challenge for - you can only rely
897             # on the identifier in the response. Even though in v1.0 we could associate domain name with this error,
898             # we treat this uniformly and return.
899 0         0 my $err = $self->_pull_error($content);
900 0         0 return $self->_status(ERROR, "Failed to receive the challenge. $err");
901             }
902             }
903 0 0       0 if (%domains_failed) {
904 0         0 my @failed = sort keys %domains_failed;
905 0         0 $self->{failed_domains} = [ \@failed ];
906 0         0 my $status = join "\n", map { "$_: $domains_failed{$_}" } @failed;
  0         0  
907 0 0       0 my $info = @failed == $domains_requested ? "All domains failed" : "Some domains failed";
908 0         0 return $self->_status(ERROR, "$info\n$status");
909             } else {
910 0         0 $self->{failed_domains} = [ undef ];
911             }
912             # Domains not requested with authz are considered to be already validated.
913 0         0 for my $domain (@{$self->{loaded_domains}}) {
  0         0  
914 0 0       0 unless (defined $self->{domains}->{$domain}) {
915 0         0 $self->{domains}->{$domain} = 1;
916 0         0 $self->_debug("Domain $domain does not require a challenge at this time.");
917             }
918             }
919 0 0       0 return $self->_status(OK, $domains_requested ? "Requested challenges for $domains_requested domain(s)." : "There are no domains which were not yet requested for challenges.");
920             }
921              
922             =head2 accept_challenge($callback [, $params] [, $type])
923              
924             Sets up a callback, which will be called for each non-verified domain to satisfy the requested challenge. Each callback will receive two parameters -
925             a hash reference with the challenge data and a hash reference of parameters optionally passed to accept_challenge(). The challenge data has the following keys:
926              
927             =over 14
928              
929             =item C<domain>
930              
931             The domain name being processed (lower-case)
932              
933             =item C<host>
934              
935             The domain name without the wildcard part (if that was present)
936              
937             =item C<token>
938              
939             The challenge token
940              
941             =item C<fingerprint>
942            
943             The account key fingerprint
944              
945             =item C<file>
946              
947             The file name for HTTP verification (essentially the same as token)
948              
949             =item C<text>
950              
951             The text for HTTP verification
952              
953             =item C<record>
954              
955             The value of the TXT record for DNS verification
956              
957             =item C<logger>
958            
959             Logger object.
960              
961             =back
962              
963             The type of the challenge accepted is optional and it is 'http' by default. The following values are currently available: 'http', 'tls', 'dns'.
964             New values which might be added by Let's Encrypt will be supported automatically. While currently all domains being processed share the same type
965             of challenge, it might be changed in the future versions.
966              
967             On error you can call failed_domains() method, which returns an array reference to domain names for which the challenge was not accepted successfully.
968              
969             The callback should return a true value on success.
970              
971             The callback could be either a code reference (for example to a subroutine in your program) or a blessed reference to a module handling
972             the challenge. In the latter case the module should have methods defined for handling appropriate challenge type, such as:
973              
974             =over
975              
976             =item
977              
978             B<handle_challenge_http()>
979              
980             =item
981              
982             B<handle_challenge_tls()>
983              
984             =item
985              
986             B<handle_challenge_dns()>
987              
988             =back
989              
990             You can use L<Crypt::LE::Challenge::Simple> example module as a template.
991            
992             Returns: OK | INVALID_DATA | ERROR.
993              
994             =cut
995              
996             sub accept_challenge {
997 0     0 1 0 my $self = shift;
998 0         0 my ($cb, $params, $type) = @_;
999 0 0 0     0 return $self->_status(ERROR, "Domains and challenges need to be set before accepting.") unless ($self->{domains} and $self->{challenges});
1000 0 0 0     0 my $mod_callback = ($cb and blessed $cb) ? 1 : 0;
1001 0   0     0 $type||='http';
1002 0         0 my $handler = "handle_challenge_$type";
1003 0 0 0     0 return $self->_status(INVALID_DATA, "Valid callback has not been provided.") unless ($cb and ((ref $cb eq 'CODE') or ($mod_callback and $cb->can($handler))));
      0        
1004 0 0 0     0 return $self->_status(INVALID_DATA, "Passed parameters are not pointing to a hash.") if ($params and (ref $params ne 'HASH'));
1005 0         0 my ($domains_accepted, @domains_failed);
1006 0         0 $self->{active_challenges} = undef;
1007 0         0 foreach my $domain (@{$self->{loaded_domains}}) {
  0         0  
1008 0 0 0     0 unless (defined $self->{domains}->{$domain} and !$self->{domains}->{$domain}) {
1009 0 0       0 $self->_debug($self->{domains}->{$domain} ? "Domain $domain has been already validated, skipping." : "Challenge has not yet been requested for domain $domain, skipping.");
1010 0         0 next;
1011             }
1012 0 0 0     0 unless ($self->{challenges}->{$domain} and $self->{challenges}->{$domain}->{$type}) {
1013 0         0 $self->_debug("Could not find a challenge of type $type for domain $domain.");
1014 0         0 push @domains_failed, $domain;
1015 0         0 next;
1016             }
1017 0         0 my $rv;
1018             my $callback_data = {
1019             domain => $domain,
1020             token => $self->{challenges}->{$domain}->{$type}->{token},
1021             fingerprint => $self->{fingerprint},
1022             logger => $self->{logger},
1023 0         0 };
1024 0         0 $self->_callback_extras($callback_data);
1025 0         0 eval {
1026 0 0       0 $rv = $mod_callback ? $cb->$handler($callback_data, $params) : &$cb($callback_data, $params);
1027             };
1028 0 0 0     0 if ($@ or !$rv) {
1029 0 0       0 $self->_debug("Challenge callback for domain $domain " . ($@ ? "thrown an error: $@" : "did not return a true value"));
1030 0         0 push @domains_failed, $domain;
1031             } else {
1032 0         0 $self->{active_challenges}->{$domain} = $type;
1033 0         0 $domains_accepted++;
1034             }
1035             }
1036 0 0       0 if (@domains_failed) {
1037 0         0 push @{$self->{failed_domains}}, \@domains_failed;
  0         0  
1038 0 0       0 return $self->_status(ERROR, $domains_accepted ? "Challenges failed for domains: " . join(", ", @domains_failed) : "All challenges failed");
1039             } else {
1040 0         0 push @{$self->{failed_domains}}, undef;
  0         0  
1041             }
1042 0 0       0 return $self->_status(OK, $domains_accepted ? "Accepted challenges for $domains_accepted domain(s)." : "There are no domains for which challenges need to be accepted.");
1043             }
1044              
1045             =head2 verify_challenge([$callback] [, $params] [, $type])
1046              
1047             Asks Let's Encrypt server to verify the results of the challenge. On error you can call failed_domains() method, which returns an array reference to domain names
1048             for which the challenge was not verified successfully.
1049              
1050             Optionally you can set up a callback, which will be called for each domain with the results of verification. The callback will receive two parameters -
1051             a hash reference with the results and a hash reference of parameters optionally passed to verify_challenge(). The results data has the following keys:
1052              
1053             =over 14
1054              
1055             =item C<domain>
1056              
1057             The domain name processed (lower-case)
1058              
1059             =item C<host>
1060              
1061             The domain name without the wildcard part (if that was present)
1062              
1063             =item C<token>
1064              
1065             The challenge token
1066              
1067             =item C<fingerprint>
1068            
1069             The account key fingerprint
1070              
1071             =item C<file>
1072              
1073             The file name for HTTP verification (essentially the same as token)
1074              
1075             =item C<text>
1076              
1077             The text for HTTP verification
1078              
1079             =item C<record>
1080              
1081             The value of the TXT record for DNS verification
1082              
1083             =item C<valid>
1084            
1085             Set to 1 if the domain has been verified successfully or set to 0 otherwise.
1086              
1087             =item C<error>
1088            
1089             Error message returned for domain on verification failure.
1090              
1091             =item C<logger>
1092            
1093             Logger object.
1094              
1095             =back
1096              
1097             The type of the challenge accepted is optional and it is 'http' by default. The following values are currently available: 'http', 'tls', 'dns'.
1098              
1099             The callback should return a true value on success.
1100              
1101             The callback could be either a code reference (for example to a subroutine in your program) or a blessed reference to a module handling
1102             the verification outcome. In the latter case the module should have methods defined for handling appropriate verification type, such as:
1103              
1104             =over
1105              
1106             =item
1107              
1108             B<handle_verification_http()>
1109              
1110             =item
1111              
1112             B<handle_verification_tls()>
1113              
1114             =item
1115              
1116             B<handle_verification_dns()>
1117              
1118             =back
1119              
1120             You can use L<Crypt::LE::Challenge::Simple> example module as a template.
1121              
1122             Returns: OK | INVALID_DATA | ERROR.
1123              
1124             =cut
1125              
1126             sub verify_challenge {
1127 0     0 1 0 my $self = shift;
1128 0         0 my ($cb, $params, $type) = @_;
1129 0 0 0     0 return $self->_status(ERROR, "Domains and challenges need to be set before verifying.") unless ($self->{domains} and $self->{challenges});
1130 0 0       0 return $self->_status(OK, "There are no active challenges to verify") unless $self->{active_challenges};
1131 0 0 0     0 my $mod_callback = ($cb and blessed $cb) ? 1 : 0;
1132 0   0     0 $type||='http';
1133 0         0 my $handler = "handle_verification_$type";
1134 0 0       0 if ($cb) {
1135 0 0 0     0 return $self->_status(INVALID_DATA, "Valid callback has not been provided.") unless ($cb and ((ref $cb eq 'CODE') or ($mod_callback and $cb->can($handler))));
      0        
1136 0 0 0     0 return $self->_status(INVALID_DATA, "Passed parameters are not pointing to a hash.") if ($params and (ref $params ne 'HASH'));
1137             }
1138 0         0 my ($domains_verified, @domains_failed);
1139 0         0 my $expected_status = $self->_compat_response(ACCEPTED);
1140 0         0 foreach my $domain (@{$self->{loaded_domains}}) {
  0         0  
1141 0 0 0     0 unless (defined $self->{domains}->{$domain} and !$self->{domains}->{$domain}) {
1142 0 0       0 $self->_debug($self->{domains}->{$domain} ? "Domain $domain has been already verified, skipping." : "Challenge has not yet been requested for domain $domain, skipping.");
1143 0         0 next;
1144             }
1145 0 0       0 unless ($self->{active_challenges}->{$domain}) {
1146 0         0 $self->_debug("Domain $domain is not set as having an active challenge (you may need to run 'accept_challenge'), skipping.");
1147 0         0 push @domains_failed, $domain;
1148 0         0 next;
1149             }
1150 0         0 my $type = delete $self->{active_challenges}->{$domain};
1151 0         0 my $token = $self->{challenges}->{$domain}->{$type}->{token};
1152 0         0 my ($status, $content) = $self->_request($self->{challenges}->{$domain}->{$type}->{uri}, { resource => 'challenge', keyAuthorization => "$token.$self->{fingerprint}" });
1153 0         0 my ($validated, $cb_reset) = (0, 0);
1154 0 0       0 if ($status == $expected_status) {
1155 0   0     0 $content->{uri} ||= $content->{url};
1156 0 0       0 if ($content->{uri}) {
1157 0         0 my @check = ($content->{uri});
1158 0 0       0 push @check, '' if ($self->version() > 1);
1159 0         0 my $try = 0;
1160 0   0     0 while ($status == $expected_status and $content and $content->{status} and $content->{status} eq 'pending') {
      0        
      0        
1161 0         0 select(undef, undef, undef, $self->{delay});
1162 0         0 ($status, $content) = $self->_request(@check);
1163 0 0 0     0 last if ($self->{try} and (++$try == $self->{try}));
1164             }
1165 0 0 0     0 if ($status == $expected_status and $content and $content->{status}) {
      0        
1166 0 0       0 if ($content->{status}=~/^(?:in)?valid$/) {
1167 0 0       0 if ($content->{status} eq 'valid') {
1168 0         0 $self->_debug("Domain $domain has been verified successfully.");
1169 0         0 $validated = 1;
1170             }
1171             }
1172             }
1173             }
1174             }
1175 0 0       0 if ($cb) {
1176 0         0 my $rv;
1177             my $callback_data = {
1178             domain => $domain,
1179             token => $self->{challenges}->{$domain}->{$type}->{token},
1180             fingerprint => $self->{fingerprint},
1181             valid => $validated,
1182             error => $self->_pull_error($content),
1183             logger => $self->{logger},
1184 0         0 };
1185 0         0 $self->_callback_extras($callback_data);
1186 0         0 eval {
1187 0 0       0 $rv = $mod_callback ? $cb->$handler($callback_data, $params) : &$cb($callback_data, $params);
1188             };
1189 0 0 0     0 if ($@ or !$rv) {
1190             # NB: Error in callback will propagate, even if validation process returned OK.
1191 0 0       0 $self->_debug("Verification callback for domain $domain " . ($@ ? "thrown an error: $@" : "did not return a true value"));
1192 0 0       0 $cb_reset = 1 if $validated;
1193 0         0 $validated = 0;
1194             }
1195             }
1196 0 0       0 if ($validated) {
1197 0         0 $self->{domains}->{$domain} = 1;
1198 0         0 $domains_verified++;
1199             } else {
1200 0 0       0 $self->_debug("Domain $domain has failed verification (status code $status).", $content) unless $cb_reset;
1201 0         0 push @domains_failed, $domain;
1202             }
1203             }
1204 0 0       0 if (@domains_failed) {
1205 0         0 push @{$self->{failed_domains}}, \@domains_failed;
  0         0  
1206 0 0       0 return $self->_status(ERROR, $domains_verified ? "Verification failed for domains: " . join(", ", @domains_failed) : "All verifications failed");
1207             } else {
1208 0         0 push @{$self->{failed_domains}}, undef;
  0         0  
1209             }
1210 0 0       0 return $self->_status(OK, $domains_verified ? "Verified challenges for $domains_verified domain(s)." : "There are no domains pending challenge verification.");
1211             }
1212              
1213             =head2 request_certificate()
1214              
1215             Requests the certificate for your CSR.
1216              
1217             Returns: OK | AUTH_ERROR | ERROR.
1218              
1219             =cut
1220              
1221             sub request_certificate {
1222 0     0 1 0 my $self = shift;
1223 0 0       0 return $self->_status(ERROR, "CSR is missing, make sure it has been either loaded or generated.") unless $self->{csr};
1224 0         0 my $csr = encode_base64url($self->pem2der($self->{csr}));
1225 0         0 my ($status, $content);
1226 0         0 delete $self->{authz};
1227 0         0 delete $self->{alternatives};
1228 0 0       0 unless ($self->{finalize}) {
1229 0         0 ($status, $content) = $self->_request($self->{directory}->{'new-cert'}, { resource => 'new-cert', csr => $csr });
1230 0 0       0 return $self->_status($status == AUTH_ERROR ? AUTH_ERROR : ERROR, $content) unless ($status == CREATED);
    0          
1231 0 0 0     0 if (ref $content eq 'HASH' and $content->{'identifiers'} and $content->{'authorizations'}) {
      0        
1232 0         0 push @{$self->{authz}}, [ $_, '' ] for @{$content->{'authorizations'}};
  0         0  
  0         0  
1233 0         0 $self->{finalize} = $content->{'finalize'};
1234             }
1235             }
1236 0 0       0 if ($self->{finalize}) {
1237             # v2. Let's attempt to finalize the order immediately.
1238 0         0 my ($ready, $try) = (0, 0);
1239 0         0 ($status, $content) = $self->_request($self->{finalize}, { csr => $csr });
1240 0   0     0 while ($status == SUCCESS and $content and $content->{status} and $content->{status} eq 'processing') {
      0        
      0        
1241 0         0 select(undef, undef, undef, $self->{delay});
1242 0         0 ($status, $content) = $self->_request($self->{finalize}, { csr => $csr });
1243 0 0 0     0 last if ($self->{try} and (++$try == $self->{try}));
1244             }
1245 0 0 0     0 if ($status == SUCCESS and $content and $content->{status}) {
      0        
1246 0 0       0 if ($content->{status} eq 'valid') {
    0          
    0          
1247 0 0       0 if ($content->{certificate}) {
1248 0         0 $self->_debug("The certificate is ready for download at $content->{certificate}.");
1249 0         0 my @cert = ($content->{certificate});
1250 0 0       0 push @cert, '' if ($self->version() > 1);
1251 0         0 ($status, $content) = $self->_request(@cert);
1252 0 0       0 return $self->_status(ERROR, "Certificate could not be downloaded from $content->{certificate}.") unless ($status == SUCCESS);
1253             # In v2 certificate is returned along with the chain.
1254 0         0 $ready = 1;
1255 0 0       0 if ($content=~/(\n\-+END CERTIFICATE\-+)[\s\r\n]+(.+)/s) {
1256 0         0 $self->_debug("Certificate is separated from the chain.");
1257 0         0 $self->{issuer} = $self->_convert($2, 'CERTIFICATE');
1258 0         0 $content = $` . $1;
1259             }
1260             # Save the links to alternative certificates.
1261 0   0     0 $self->{alternatives} = $self->{links}->{alternate} || [];
1262             } else {
1263 0         0 return $self->_status(ERROR, "The certificate is ready, but there was no download link provided.");
1264             }
1265             } elsif ($content->{status} eq 'invalid') {
1266 0         0 return $self->_status(ERROR, "Certificate cannot be issued.");
1267             } elsif ($content->{status} eq 'pending') {
1268 0         0 return $self->_status(AUTH_ERROR, "Order already exists but not yet completed.");
1269             } else {
1270 0         0 return $self->_status(ERROR, "Unknown order status: $content->{status}.");
1271             }
1272             } else {
1273 0         0 return $self->_status(AUTH_ERROR, "Could not finalize an order.");
1274             }
1275 0 0       0 return $self->_status(AUTH_ERROR, "Could not finalize an order.") unless $ready;
1276             }
1277 0         0 $self->{certificate} = $self->_convert($content, 'CERTIFICATE');
1278 0         0 $self->{certificate_url} = $self->{location};
1279 0 0 0     0 $self->{issuer_url} = ($self->{links} and $self->{links}->{up}) ? $self->{links}->{up} : undef;
1280 0 0       0 return $self->_status(OK, "Domain certificate has been received." . ($self->{issuer_url} ? " Issuer's certificate can be found at: $self->{issuer_url}" : ""));
1281             }
1282              
1283             =head2 request_alternatives()
1284              
1285             Requests alternative certificates if any are available.
1286              
1287             Returns: OK | ERROR.
1288              
1289             =cut
1290              
1291             sub request_alternatives {
1292 0     0 1 0 my $self = shift;
1293 0 0       0 return $self->_status(ERROR, "The default certificate must be requested before the alternatives.") unless $self->{alternatives};
1294 0         0 my ($status, $content);
1295 0         0 delete $self->{alternative_certificates};
1296 0         0 foreach my $link (@{$self->{alternatives}}) {
  0         0  
1297 0         0 $self->_debug("Alternative certificate is available at $link.");
1298 0         0 my @cert = ($link);
1299 0 0       0 push @cert, '' if ($self->version() > 1);
1300 0         0 ($status, $content) = $self->_request(@cert);
1301 0 0       0 return $self->_status(ERROR, "Certificate could not be downloaded from $link.") unless ($status == SUCCESS);
1302             # In v2 certificate is returned along with the chain.
1303 0 0       0 if ($content=~/(\n\-+END CERTIFICATE\-+)[\s\r\n]+(.+)/s) {
1304 0         0 $self->_debug("Certificate is separated from the chain.");
1305 0         0 push @{$self->{alternative_certificates}}, [ $self->_convert($` . $1, 'CERTIFICATE'), $self->_convert($2, 'CERTIFICATE') ];
  0         0  
1306             } else {
1307 0         0 push @{$self->{alternative_certificates}}, [ $self->_convert($content, 'CERTIFICATE') ];
  0         0  
1308             }
1309             }
1310 0         0 return $self->_status(OK, "Alternative certificates have been received.");
1311             }
1312              
1313             =head2 request_issuer_certificate()
1314              
1315             Requests the issuer's certificate.
1316              
1317             Returns: OK | ERROR.
1318              
1319             =cut
1320              
1321             sub request_issuer_certificate {
1322 0     0 1 0 my $self = shift;
1323 0 0       0 return $self->_status(OK, "Issuer's certificate has been already received.") if $self->issuer();
1324 0 0       0 return $self->_status(ERROR, "The URL of issuer certificate is not set.") unless $self->{issuer_url};
1325 0         0 my ($status, $content) = $self->_request($self->{issuer_url});
1326 0 0       0 if ($status == SUCCESS) {
1327 0         0 $self->{issuer} = $self->_convert($content, 'CERTIFICATE');
1328 0         0 return $self->_status(OK, "Issuer's certificate has been received.");
1329             }
1330 0         0 return $self->_status(ERROR, $content);
1331             }
1332              
1333             =head2 revoke_certificate($certificate_file|$scalar_ref)
1334              
1335             Revokes a certificate.
1336              
1337             Returns: OK | READ_ERROR | ALREADY_DONE | ERROR.
1338              
1339             =cut
1340              
1341             sub revoke_certificate {
1342 0     0 1 0 my $self = shift;
1343 0         0 my $file = shift;
1344 0         0 my $crt = $self->_file($file);
1345 0 0       0 return $self->_status(READ_ERROR, "Certificate reading error.") unless $crt;
1346 0         0 my ($status, $content) = $self->_request($self->{directory}->{'revoke-cert'},
1347             { resource => 'revoke-cert', certificate => encode_base64url($self->pem2der($crt)) },
1348             { jwk => 0 });
1349 0 0       0 if ($status == SUCCESS) {
    0          
1350 0         0 return $self->_status(OK, "Certificate has been revoked.");
1351             } elsif ($status == ALREADY_DONE) {
1352 0         0 return $self->_status(ALREADY_DONE, "Certificate has been already revoked.");
1353             }
1354 0         0 return $self->_status(ERROR, $content);
1355             }
1356              
1357             #====================================================================================================
1358             # API Workflow helpers
1359             #====================================================================================================
1360              
1361             =head1 METHODS (Other)
1362              
1363             The following methods are the common getters you can use to get more details about the outcome of the workflow run and return some retrieved data, such as
1364             registration info and certificates for your domains.
1365              
1366             =head2 tos()
1367              
1368             Returns: The link to a Terms of Service document or undef.
1369              
1370             =cut
1371              
1372             sub tos {
1373 0     0 1 0 my $self = shift;
1374 0 0 0     0 return ($self->{links} and $self->{links}->{'terms-of-service'}) ? $self->{links}->{'terms-of-service'} : undef;
1375             }
1376              
1377             =head2 tos_changed()
1378              
1379             Returns: True if Terms of Service have been changed (or you haven't yet accepted them). Otherwise returns false.
1380              
1381             =cut
1382              
1383             sub tos_changed {
1384 0     0 1 0 return shift->{tos_changed};
1385             }
1386              
1387             =head2 new_registration()
1388              
1389             Returns: True if new key has been registered. Otherwise returns false.
1390              
1391             =cut
1392              
1393             sub new_registration {
1394 0     0 1 0 return shift->{new_registration};
1395             }
1396              
1397             =head2 registration_info()
1398              
1399             Returns: Registration information structure returned by Let's Encrypt for your key or undef.
1400              
1401             =cut
1402              
1403             sub registration_info {
1404 0     0 1 0 return shift->{registration_info};
1405             }
1406              
1407             =head2 registration_id()
1408              
1409             Returns: Registration ID returned by Let's Encrypt for your key or undef.
1410              
1411             =cut
1412              
1413             sub registration_id {
1414 0     0 1 0 return shift->{registration_id};
1415             }
1416              
1417             =head2 contact_details()
1418              
1419             Returns: Contact details returned by Let's Encrypt for your key or undef.
1420              
1421             =cut
1422              
1423             sub contact_details {
1424 0     0 1 0 return shift->{contact_details};
1425             }
1426              
1427             =head2 certificate()
1428              
1429             Returns: The last received certificate or undef.
1430              
1431             =cut
1432              
1433             sub certificate {
1434 0     0 1 0 return shift->{certificate};
1435             }
1436              
1437             =head2 alternative_certificate()
1438              
1439             Returns: Specific alternative certificate as an arrayref (domain, issuer) or undef.
1440              
1441             =cut
1442              
1443             sub alternative_certificate {
1444 0     0 1 0 my ($self, $idx) = @_;
1445 0 0 0     0 if ($self->{alternative_certificates} and defined $idx and $idx < @{$self->{alternative_certificates}}) {
  0   0     0  
1446 0         0 return $self->{alternative_certificates}->[$idx];
1447             }
1448 0         0 return undef;
1449             }
1450              
1451             =head2 alternative_certificates()
1452              
1453             Returns: All available alternative certificates (as an arrayref of arrayrefs) or undef.
1454              
1455             =cut
1456              
1457             sub alternative_certificates {
1458 0     0 1 0 my ($self) = @_;
1459 0 0       0 if ($self->{alternative_certificates}) {
1460             # Prevent them from being accidentally changed (using the core module to avoid adding more dependencies).
1461 0         0 return dclone $self->{alternative_certificates};
1462             }
1463 0         0 return undef;
1464             }
1465              
1466             =head2 certificate_url()
1467              
1468             Returns: The URL of the last received certificate or undef.
1469              
1470             =cut
1471              
1472             sub certificate_url {
1473 0     0 1 0 return shift->{certificate_url};
1474             }
1475              
1476             =head2 issuer()
1477              
1478             Returns: The issuer's certificate or undef.
1479              
1480             =cut
1481              
1482             sub issuer {
1483 0     0 1 0 return shift->{issuer};
1484             }
1485              
1486             =head2 issuer_url()
1487              
1488             Returns: The URL of the issuer's certificate or undef.
1489              
1490             =cut
1491              
1492             sub issuer_url {
1493 0     0 1 0 return shift->{issuer_url};
1494             }
1495              
1496             =head2 domains()
1497              
1498             Returns: An array reference to the loaded domain names or undef.
1499              
1500             =cut
1501              
1502             sub domains {
1503 5     5 1 41 return shift->{loaded_domains};
1504             }
1505              
1506             =head2 failed_domains([$all])
1507              
1508             Returns: An array reference to the domain names for which processing has failed or undef. If any true value is passed as a parameter, then the list
1509             will contain domain names which failed on any of the request/accept/verify steps. Otherwise the list will contain the names of the domains failed on
1510             the most recently called request/accept/verify step.
1511              
1512             =cut
1513              
1514             sub failed_domains {
1515 2     2 1 5 my ($self, $all) = @_;
1516 2 50 33     8 return undef unless ($self->{failed_domains} and @{$self->{failed_domains}});
  2         8  
1517 2 100       10 return $self->{failed_domains}->[-1] unless $all;
1518 1         2 my %totals;
1519 1         2 foreach my $proc (@{$self->{failed_domains}}) {
  1         4  
1520 2 100       5 if ($proc) {
1521 1         2 $totals{$_} = undef for @{$proc};
  1         5  
1522             }
1523             }
1524 1         10 my @rv = sort keys %totals;
1525 1 50       9 return @rv ? \@rv : undef;
1526             }
1527              
1528             =head2 verified_domains()
1529              
1530             Returns: An array reference to the successfully verified domain names.
1531              
1532             =cut
1533              
1534             sub verified_domains {
1535 1     1 1 3 my $self = shift;
1536 1 50 33     7 return undef unless ($self->{domains} and %{$self->{domains}});
  1         6  
1537 1         3 my @list = grep { $self->{domains}->{$_} } keys %{$self->{domains}};
  3         8  
  1         4  
1538 1 50       10 return @list ? \@list : undef;
1539             }
1540              
1541             =head2 check_expiration($certificate_file|$scalar_ref|$url, [ \%params ])
1542              
1543             Checks the expiration of the certificate. Accepts an URL, a full path to the certificate file or a
1544             scalar reference to a certificate in memory. Optionally a hash ref of parameters can be provided with the
1545             timeout key set to the amount of seconds to wait for the https checks (by default set to 10 seconds).
1546              
1547             Returns: Days left until certificate expiration or undef on error. Note - zero and negative values can be
1548             returned for the already expired certificates. On error the status is set accordingly to one of the following:
1549             INVALID_DATA, LOAD_ERROR or ERROR, and the 'error_details' call can be used to get more information about the problem.
1550              
1551             =cut
1552              
1553             sub check_expiration {
1554 2     2 1 340 my ($self, $res, $params) = @_;
1555 2         5 my ($load_error, $exp);
1556 2 50 33     8 my $timeout = $params->{timeout} if ($params and (ref $params eq 'HASH'));
1557 2 50 0     19 if (!$res or ($timeout and ($timeout!~/^\d+/ or $timeout < 1))) {
    50 33        
      33        
      33        
1558 0         0 $self->_status(INVALID_DATA, "Invalid parameters");
1559 0         0 return undef;
1560             } elsif (ref $res or $res!~m~^\w+://~i) {
1561 2         4 my $bio;
1562 2 50       6 if (ref $res) {
1563 2         30 $bio = Net::SSLeay::BIO_new(Net::SSLeay::BIO_s_mem());
1564 2 50 33     45 $load_error = 1 unless ($bio and Net::SSLeay::BIO_write($bio, $$res));
1565             } else {
1566 0         0 $bio = Net::SSLeay::BIO_new_file($res, 'r');
1567 0 0       0 $load_error = 1 unless $bio;
1568             }
1569 2 50       8 unless ($load_error) {
1570 2         66 my $cert = Net::SSLeay::PEM_read_bio_X509($bio);
1571 2         9 Net::SSLeay::BIO_free($bio);
1572 2 100       8 unless ($cert) {
1573 1         4 $self->_status(LOAD_ERROR, "Could not parse the certificate");
1574 1         7 return undef;
1575             }
1576 1         7 _verify_crt(\$exp)->(0, 0, 0, 0, $cert, 0);
1577             } else {
1578 0         0 $self->_status(LOAD_ERROR, "Could not load the certificate");
1579 0         0 return undef;
1580             }
1581             } else {
1582 0         0 $res=~s/^[^:]+/https/;
1583 0   0     0 my $probe = HTTP::Tiny->new(
1584             agent => "Mozilla/5.0 (compatible; Crypt::LE v$VERSION agent; https://Do-Know.com/)",
1585             verify_SSL => 1,
1586             timeout => $timeout || 10,
1587             SSL_options => { SSL_verify_callback => _verify_crt(\$exp) },
1588             );
1589 0         0 my $response = $probe->head($res);
1590 0 0 0     0 $self->_status(ERROR, "Connection error: $response->{status} " . ($response->{reason}||'')) unless $response->{success};
1591             }
1592 1         12 return $exp;
1593             }
1594              
1595             =head2 pem2der($pem)
1596              
1597             Returns: DER form of the provided PEM content
1598              
1599             =cut
1600              
1601             sub pem2der {
1602 3     3 1 397 my ($self, $pem) = @_;
1603 3 50       10 return unless $pem;
1604 3 50       246 $pem = $1 if $pem=~/(?:^|\s+)-+BEGIN[^-]*-+\s+(.*?)\s+-+END/s;
1605 3         18 $pem=~s/\s+//;
1606 3         44 return decode_base64($pem);
1607             }
1608              
1609             =head2 der2pem($der, $type)
1610              
1611             Returns: PEM form of the provided DER content of the given type (for example 'CERTIFICATE REQUEST') or undef.
1612              
1613             =cut
1614              
1615             sub der2pem {
1616 3     3 1 11 my ($self, $der, $type) = @_;
1617 3 50 33     80 return ($der and $type) ? "-----BEGIN $type-----$/" . encode_base64($der) . "-----END $type-----" : undef;
1618             }
1619              
1620             =head2 export_pfx($file, $pass, $cert, $key, [ $ca ], [ $tag ])
1621              
1622             Exports given certificate, CA chain and a private key into a PFX/P12 format with a given password.
1623             Optionally you can specify a text to go into pfx instead of the default "Crypt::LE exported".
1624              
1625             Returns: OK | UNSUPPORTED | INVALID_DATA | ERROR.
1626              
1627             =cut
1628              
1629             sub export_pfx {
1630 1     1 1 362 my ($self, $file, $pass, $cert, $key, $ca, $tag) = @_;
1631 1         3 my $unsupported = "PFX export is not supported (requires specific build of PKCS12 library for Windows).";
1632 1 50       6 return $self->_status(UNSUPPORTED, $unsupported) unless $pkcs12_available;
1633 0 0       0 return $self->_status(INVALID_DATA, "Password is required") unless $pass;
1634 0         0 my $pkcs12 = Crypt::OpenSSL::PKCS12->new();
1635 0         0 eval {
1636 0   0     0 $pkcs12->create($cert, $key, $pass, $file, $ca, $tag || "Crypt::LE exported");
1637             };
1638 0 0 0     0 return $self->_status(UNSUPPORTED, $unsupported) if ($@ and $@=~/Usage/);
1639 0 0       0 return $self->_status(ERROR, $@) if $@;
1640 0         0 return $self->_status(OK, "PFX exported to $file.");
1641             }
1642              
1643             =head2 error()
1644              
1645             Returns: Last error (can be a code or a structure) or undef.
1646              
1647             =cut
1648              
1649             sub error {
1650 0     0 1 0 return shift->{error};
1651             }
1652              
1653             =head2 error_details()
1654              
1655             Returns: Last error details if available or a generic 'error' string otherwise. Empty string if the last called method returned OK.
1656              
1657             =cut
1658              
1659             sub error_details {
1660 1     1 1 10 my $self = shift;
1661 1 50       4 if ($self->{error}) {
1662 0         0 my $err = $self->_pull_error($self->{error});
1663 0 0       0 return $err ? $err : (ref $self->{error}) ? 'error' : $self->{error};
    0          
1664             }
1665 1         9 return '';
1666             }
1667              
1668             #====================================================================================================
1669             # Internal Crypto helpers
1670             #====================================================================================================
1671              
1672             sub _key {
1673 10     10   23 my ($key, $type, $attr) = @_;
1674 10         15 my $pk;
1675 10   100     37 $type||=KEY_RSA;
1676 10 50 33     68 return (undef, "Unsupported key type", INVALID_DATA) unless ($type=~/^\d+$/ and $type <= KEY_ECC);
1677 10 100       30 if ($type == KEY_RSA) {
    50          
1678 8   66     29 $attr||=$keysize;
1679 8 100 100     45 return (undef, "Unsupported key size", INVALID_DATA) if ($attr < 2048 or $attr%1024);
1680             } elsif ($type == KEY_ECC) {
1681 2 100 66     12 $attr = $keycurve unless ($attr and $attr ne 'default');
1682 2 50       6 return (undef, "Unsupported key type - upgrade Net::SSLeay to version 1.75 or better", UNSUPPORTED) unless defined &Net::SSLeay::EC_KEY_generate_key;
1683             }
1684 8 100       18 if ($key) {
1685 4         22 my $bio = Net::SSLeay::BIO_new(Net::SSLeay::BIO_s_mem());
1686 4 50       12 return (undef, "Could not allocate memory for the key") unless $bio;
1687 4 50       40 return _free(b => $bio, error => "Could not load the key data") unless Net::SSLeay::BIO_write($bio, $key);
1688 4         301 $pk = Net::SSLeay::PEM_read_bio_PrivateKey($bio);
1689 4         15 _free(b => $bio);
1690 4 50       11 return (undef, "Could not read the private key") unless $pk;
1691             } else {
1692 4         19 $pk = Net::SSLeay::EVP_PKEY_new();
1693 4 50       22 return (undef, "Could not allocate memory for the key") unless $pk;
1694 4         6 my $gen;
1695 4         8 eval {
1696 4 100       238 $gen = ($type == KEY_RSA) ? Net::SSLeay::RSA_generate_key($attr, &Net::SSLeay::RSA_F4) : Net::SSLeay::EC_KEY_generate_key($attr);
1697             };
1698 4 100       1151427 $@=~s/ at \S+ line \d+.$// if $@;
1699 4 50       22 return _free(k => $pk, error => "Could not generate the private key '$attr'" . ($@ ? " - $@" : "")) unless $gen;
    100          
1700 3 100       37 ($type == KEY_RSA) ? Net::SSLeay::EVP_PKEY_assign_RSA($pk, $gen) : Net::SSLeay::EVP_PKEY_assign_EC_KEY($pk, $gen);
1701             }
1702 7         33 return ($pk);
1703             }
1704              
1705             sub _csr {
1706 6     6   15 my ($pk, $domains, $attrib) = @_;
1707 6         16 my $ref = ref $domains;
1708 6 50 33     42 return unless ($domains and (!$ref or $ref eq 'ARRAY'));
      33        
1709 6 50 33     30 return if ($attrib and (ref $attrib ne 'HASH'));
1710 6         39 my $req = Net::SSLeay::X509_REQ_new();
1711 6 50       14 return _free(k => $pk) unless $req;
1712 6 50       90 return _free(k => $pk, r => $req) unless (Net::SSLeay::X509_REQ_set_pubkey($req, $pk));
1713 6 50       15 my @names = $ref ? @{$domains} : split(/\s*,\s*/, $domains);
  6         50  
1714 6 50 33     55 $attrib->{CN} = $names[0] unless ($attrib and ($attrib->{CN} or $attrib->{commonName}));
      33        
1715 6         17 my $list = join ',', map { 'DNS:' . encode_utf8($_) } @names;
  8         57  
1716 6 50       192 return _free(k => $pk, r => $req) unless Net::SSLeay::P_X509_REQ_add_extensions($req, &Net::SSLeay::NID_subject_alt_name => $list);
1717 6         247 my $n = Net::SSLeay::X509_NAME_new();
1718 6 50       13 return _free(k => $pk, r => $req) unless $n;
1719 6         11 foreach my $key (keys %{$attrib}) {
  6         32  
1720             # Can use long or short names
1721 30 50       912 return _free(k => $pk, r => $req) unless Net::SSLeay::X509_NAME_add_entry_by_txt($n, $key, MBSTRING_UTF8, encode_utf8($attrib->{$key}));
1722             }
1723 6 50       276 return _free(k => $pk, r => $req) unless Net::SSLeay::X509_REQ_set_subject_name($req, $n);
1724             # Handle old openssl and set the version explicitly unless it is set already to greater than v1 (0 value).
1725             # NB: get_version will return 0 regardless of whether version is set to v1 or not set at all.
1726 6 50       24 unless (Net::SSLeay::X509_REQ_get_version($req)) {
1727 6 50       19 return _free(k => $pk, r => $req) unless Net::SSLeay::X509_REQ_set_version($req, 0);
1728             }
1729 6         30 my $md = Net::SSLeay::EVP_get_digestbyname('sha256');
1730 6 50 33     78746 return _free(k => $pk, r => $req) unless ($md and Net::SSLeay::X509_REQ_sign($req, $pk, $md));
1731 6         807 my @rv = (Net::SSLeay::PEM_get_string_X509_REQ($req), Net::SSLeay::PEM_get_string_PrivateKey($pk));
1732 6         29 _free(k => $pk, r => $req);
1733 6         32 return @rv;
1734             }
1735              
1736             sub _free {
1737 22     22   405 my %data = @_;
1738 22 100       111 Net::SSLeay::X509_REQ_free($data{r}) if $data{r};
1739 22 100       125 Net::SSLeay::BIO_free($data{b}) if $data{b};
1740 22 100       81 Net::SSLeay::EVP_PKEY_free($data{k}) if $data{k};
1741 22 100       74 return wantarray ? (undef, $data{'error'}) : undef;
1742             }
1743              
1744             sub _to_hex {
1745 6     6   64 my $val = shift;
1746 6         35 $val = $val->to_hex;
1747 6         13 $val =~s/^0x//;
1748 6 50       18 $val = "0$val" if length($val) % 2;
1749 6         64 return $val;
1750             }
1751              
1752             #====================================================================================================
1753             # Internal Service helpers
1754             #====================================================================================================
1755              
1756             sub _request {
1757 2     2   5 my $self = shift;
1758 2         6 my ($url, $payload, $opts) = @_;
1759 2 50       8 unless ($url) {
1760 0         0 my $rv = 'Resource directory does not contain expected fields.';
1761 0 0       0 return wantarray ? (INVALID_DATA, $rv) : $rv;
1762             }
1763 2         11 $self->_debug("Connecting to $url");
1764 2         14 $payload = $self->_translate($payload);
1765 2         5 my $resp;
1766 2   100     11 $opts ||= {};
1767 2   100     14 my $method = lc($opts->{method} || 'get');
1768 2 50 33     17 if (defined $payload or $method eq 'post') {
1769             $resp = defined $payload ? $self->{ua}->post($url, { headers => $headers, content => $self->_jws($payload, $url, $opts) }) :
1770 0 0       0 $self->{ua}->post($url, { headers => $headers });
1771             } else {
1772 2         86 $resp = $self->{ua}->$method($url);
1773             }
1774 2 100 66     720452 my $slurp = ($resp->{headers}->{'content-type'} and $resp->{headers}->{'content-type'}=~/^application\/(?:problem\+)?json/) ? 0 : 1;
1775 2 100       21 $self->_debug($slurp ? $resp->{headers} : $resp);
1776 2 100       11 $self->{nonce} = $resp->{headers}->{'replay-nonce'} if $resp->{headers}->{'replay-nonce'};
1777 2         10 my ($status, $rv) = ($resp->{status}, $resp->{content});
1778 2 100       7 unless ($slurp) {
1779 1         4 eval {
1780 1         27 $rv = $j->decode($rv);
1781             };
1782 1 50       6 if ($@) {
1783 0         0 ($status, $rv) = (ERROR, $@);
1784             }
1785             }
1786 2 100       14 $self->{links} = $resp->{headers}->{link} ? $self->_links($resp->{headers}->{link}) : undef;
1787 2 50       10 $self->{location} = $resp->{headers}->{location} ? $resp->{headers}->{location} : undef;
1788 2 100       19 return wantarray ? ($status, $rv) : $rv;
1789             }
1790              
1791             sub _jwk {
1792 3     3   5 my $self = shift;
1793 3 50       10 return unless $self->{key_params};
1794             return {
1795             kty => "RSA",
1796             n => encode_base64url(pack("H*", _to_hex($self->{key_params}->{n}))),
1797 3         11 e => encode_base64url(pack("H*", _to_hex($self->{key_params}->{e}))),
1798             };
1799             }
1800              
1801             sub _jws {
1802 0     0   0 my $self = shift;
1803 0         0 my ($obj, $url, $opts) = @_;
1804 0 0       0 return unless (defined $obj);
1805 0 0       0 my $json = ref $obj ? encode_base64url($j->encode($obj)) : "";
1806 0         0 my $protected = { alg => "RS256", jwk => $self->{jwk}, nonce => $self->{nonce} };
1807 0   0     0 $opts ||= {};
1808 0 0 0     0 if ($url and $self->version() > 1) {
1809 0 0 0     0 if ($self->{directory}->{reg} and !$opts->{jwk}) {
1810 0         0 $protected->{kid} = $self->{directory}->{reg};
1811 0         0 delete $protected->{jwk};
1812             }
1813 0         0 $protected->{url} = $url;
1814             }
1815 0         0 my $header = encode_base64url($j->encode($protected));
1816 0         0 my $sig = encode_base64url($self->{key}->sign("$header.$json"));
1817 0         0 my $jws = $j->encode({ protected => $header, payload => $json, signature => $sig });
1818 0         0 return $jws;
1819             }
1820              
1821             sub _links {
1822 1     1   2 my $self = shift;
1823 1         3 my ($links) = @_;
1824 1 50       39 return unless $links;
1825 1         3 my $rv;
1826 1 50       8 foreach my $link ((ref $links eq 'ARRAY') ? @{$links} : ($links)) {
  0         0  
1827 1 50 33     16 next unless ($link and $link=~/^<([^>]+)>;rel="([^"]+)"$/i);
1828 1 50       5 if ($2 eq 'alternate') {
1829             # We might have more than one alternate link.
1830 0         0 push @{$rv->{$2}}, $1;
  0         0  
1831             } else {
1832 1         6 $rv->{$2} = $1;
1833             }
1834             }
1835 1         4 return $rv;
1836             }
1837              
1838             sub _compat {
1839 1     1   3 my ($self, $content) = @_;
1840 1 50       4 return unless $content;
1841 1         2 foreach (keys %{$content}) {
  1         9  
1842 7 100       18 if (my $name = $compat->{$_}) {
1843 3         11 $content->{$name} = delete $content->{$_};
1844             }
1845             }
1846             }
1847              
1848             sub _compat_response {
1849 0     0   0 my ($self, $code) = @_;
1850 0 0       0 return ($self->version() == 2) ? SUCCESS : $code;
1851             }
1852              
1853             sub _translate {
1854 2     2   5 my ($self, $req) = @_;
1855 2 0 33     8 return $req if (!$req or $self->version() == 1 or !$req->{'resource'});
      33        
1856 0 0       0 return $req unless my $res = delete $req->{'resource'};
1857 0 0 0     0 if ($res eq 'new-reg' or $res eq 'reg') {
    0          
1858 0         0 delete $req->{'agreement'};
1859 0         0 $req->{'termsOfServiceAgreed'} = \1;
1860             } elsif ($res eq 'new-cert') {
1861 0         0 delete $req->{'csr'};
1862 0         0 push @{$req->{'identifiers'}}, { type => 'dns', value => $_ } for @{$self->{loaded_domains}};
  0         0  
  0         0  
1863             }
1864 0         0 return $req;
1865             }
1866              
1867             sub _callback_extras {
1868 0     0   0 my ($self, $data) = @_;
1869 0 0 0     0 return unless ($data and $data->{domain});
1870 0         0 $data->{domain}=~/^(\*\.)?(.+)$/;
1871 0         0 $data->{host} = $2;
1872 0         0 $data->{file} = $data->{token};
1873 0         0 $data->{text} = "$data->{token}.$data->{fingerprint}";
1874 0         0 $data->{record} = encode_base64url(sha256($data->{text}));
1875             }
1876              
1877             sub _debug {
1878 58     58   93 my $self = shift;
1879 58 50       152 return unless $self->{debug};
1880 0         0 foreach (@_) {
1881 0 0       0 if (!ref $_) {
    0          
1882 0 0       0 $self->{logger} ? $self->{logger}->debug($_) : print "$_\n";
1883             } elsif ($self->{debug} > 1) {
1884 0 0       0 $self->{logger} ? $self->{logger}->debug(Dumper($_)) : print Dumper($_);
1885             }
1886             }
1887             }
1888              
1889             sub _status {
1890 46     46   87 my $self = shift;
1891 46         101 my ($code, $data) = @_;
1892 46 100       135 if ($code == OK) {
1893 22         42 undef $self->{error};
1894             } else {
1895 24 50 33     69 if (ref $data eq 'HASH' and $data->{error}) {
1896 0         0 $self->{error} = $data->{error};
1897             } else {
1898 24   33     73 $self->{error} = $data||$code;
1899             }
1900             }
1901 46 50       151 $self->_debug($data) if $data;
1902 46         262 return $code;
1903             }
1904              
1905             sub _pull_error {
1906 0     0   0 my $self = shift;
1907 0         0 my ($err) = @_;
1908 0 0 0     0 if ($err and ref $err eq 'HASH') {
1909 0 0 0     0 return $err->{error}->{detail} if ($err->{error} and $err->{error}->{detail});
1910 0 0       0 return $err->{detail} if $err->{detail};
1911             }
1912 0         0 return '';
1913             }
1914              
1915             sub _get_authz {
1916 0     0   0 my $self = shift;
1917 0 0       0 return unless $self->{loaded_domains};
1918 0         0 $self->{authz} = [];
1919 0         0 foreach my $domain (@{$self->{loaded_domains}}) {
  0         0  
1920 0         0 push @{$self->{authz}}, [ $self->{directory}->{'new-authz'}, { resource => 'new-authz', identifier => { type => 'dns', value => $domain } } ];
  0         0  
1921             }
1922             }
1923              
1924             sub _file {
1925 18     18   29 my $self = shift;
1926 18         28 my ($file) = @_;
1927 18 50       47 return unless $file;
1928 18 100       40 unless (ref $file) {
1929 4         28 my ($fh, $content) = (new IO::File "<$file");
1930 4 50       370 if (defined $fh) {
1931 4         17 local $/;
1932 4         14 $fh->binmode;
1933 4         170 $content = <$fh>;
1934 4         29 $fh->close;
1935             }
1936 4         107 return $content;
1937             }
1938 14 50       50 return (ref $file eq 'SCALAR') ? $$file : undef;
1939             }
1940              
1941             sub _verify_crt {
1942 1     1   2 my $exp = shift;
1943             return sub {
1944 1 50 33 1   8 unless (defined $_[CRT_DEPTH] and $_[CRT_DEPTH]) {
1945 1         3 my ($t, $s);
1946 1         41 eval {
1947 1         7 $t = Net::SSLeay::X509_get_notAfter($_[PEER_CRT]);
1948 1         22 $t = Time::Piece->strptime(Net::SSLeay::P_ASN1_TIME_get_isotime($t), "%Y-%m-%dT%H:%M:%SZ");
1949             };
1950 1 50       116 unless ($@) {
1951 1         6 $s = $t - localtime;
1952 1         273 $s = int($s->days);
1953 1 50 33     60 $$exp = $s unless ($$exp and $s > $$exp);
1954             }
1955             }
1956 1         11 };
1957             }
1958              
1959             sub _convert {
1960 3     3   5 my $self = shift;
1961 3         7 my ($content, $type) = @_;
1962 3 100 66     140 return (!$content or $content=~/^\-+BEGIN/) ? $content : $self->der2pem($content, $type);
1963             }
1964              
1965             1;
1966              
1967             =head1 AUTHOR
1968              
1969             Alexander Yezhov, C<< <leader at cpan.org> >>
1970             Domain Knowledge Ltd.
1971             L<https://do-know.com/>
1972              
1973             =head1 BUGS
1974              
1975             Considering that this module has been written in a rather quick manner after I decided to give a go to Let's Encrypt certificates
1976             and found that CPAN seems to be lacking some easy ways to leverage LE API from Perl, expect some (hopefully minor) bugs.
1977             The initial goal was to make this work, make it easy to use and possibly remove the need to use openssl command line.
1978              
1979             Please report any bugs or feature requests to C<bug-crypt-le at rt.cpan.org>, or through
1980             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Crypt-LE>. I will be notified, and then you'll
1981             automatically be notified of progress on your bug as I make changes.
1982              
1983             =head1 SUPPORT
1984              
1985             You can find documentation for this module with the perldoc command.
1986              
1987             perldoc Crypt::LE
1988              
1989              
1990             You can also look for information at:
1991              
1992             =over 4
1993              
1994             =item * RT: CPAN's request tracker (report bugs here)
1995              
1996             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Crypt-LE>
1997              
1998             =item * AnnoCPAN: Annotated CPAN documentation
1999              
2000             L<http://annocpan.org/dist/Crypt-LE>
2001              
2002             =item * CPAN Ratings
2003              
2004             L<http://cpanratings.perl.org/d/Crypt-LE>
2005              
2006             =item * Search CPAN
2007              
2008             L<http://search.cpan.org/dist/Crypt-LE/>
2009              
2010             =item * Project homepage
2011              
2012             L<https://Do-Know.com/>
2013              
2014              
2015              
2016             =back
2017              
2018             =head1 LICENSE AND COPYRIGHT
2019              
2020             Copyright 2016-2020 Alexander Yezhov.
2021              
2022             This program is free software; you can redistribute it and/or modify it
2023             under the terms of the Artistic License (2.0). You may obtain a
2024             copy of the full license at:
2025              
2026             L<http://www.perlfoundation.org/artistic_license_2_0>
2027              
2028             Any use, modification, and distribution of the Standard or Modified
2029             Versions is governed by this Artistic License. By using, modifying or
2030             distributing the Package, you accept this license. Do not use, modify,
2031             or distribute the Package, if you do not accept this license.
2032              
2033             If your Modified Version has been derived from a Modified Version made
2034             by someone other than you, you are nevertheless required to ensure that
2035             your Modified Version complies with the requirements of this license.
2036              
2037             This license does not grant you the right to use any trademark, service
2038             mark, tradename, or logo of the Copyright Holder.
2039              
2040             This license includes the non-exclusive, worldwide, free-of-charge
2041             patent license to make, have made, use, offer to sell, sell, import and
2042             otherwise transfer the Package with respect to any patent claims
2043             licensable by the Copyright Holder that are necessarily infringed by the
2044             Package. If you institute patent litigation (including a cross-claim or
2045             counterclaim) against any party alleging that the Package constitutes
2046             direct or contributory patent infringement, then this Artistic License
2047             to you shall terminate on the date that such litigation is filed.
2048              
2049             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
2050             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
2051             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
2052             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
2053             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
2054             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
2055             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
2056             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2057              
2058              
2059             =cut
2060