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