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