File Coverage

blib/lib/Crypt/LE.pm
Criterion Covered Total %
statement 410 839 48.8
branch 162 540 30.0
condition 56 349 16.0
subroutine 61 93 65.5
pod 46 46 100.0
total 735 1867 39.3


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