File Coverage

blib/lib/Net/ACME2.pm
Criterion Covered Total %
statement 103 149 69.1
branch 17 34 50.0
condition 13 24 54.1
subroutine 30 46 65.2
pod 11 11 100.0
total 174 264 65.9


line stmt bran cond sub pod time code
1             package Net::ACME2;
2              
3 3     3   513 use strict;
  3         7  
  3         89  
4 3     3   14 use warnings;
  3         6  
  3         124  
5              
6             our $VERSION;
7             BEGIN {
8 3     3   142 $VERSION = '0.40_02';
9             }
10              
11             =encoding utf-8
12              
13             =head1 NAME
14              
15             Net::ACME2 - Client logic for the ACME (Let's Encrypt) protocol
16              
17             X X X
18              
19             =head1 SYNOPSIS
20              
21             package SomeCA::ACME;
22              
23             use parent qw( Net::ACME2 );
24              
25             use constant {
26             DIRECTORY_PATH => '/acme-directory',
27             };
28              
29             # %opts are the parameters given to new().
30             sub HOST {
31             my ($class, %opts) = @_;
32              
33             # You can make this depend on the %opts if you want.
34             return 'acme.someca.net';
35             }
36              
37             package main;
38              
39             my $acme = SomeCA::ACME->new(
40             key => $account_key_pem_or_der,
41             key_id => undef,
42             );
43              
44             #for a new account
45             {
46             my $terms_url = $acme->get_terms_of_service();
47              
48             $acme->create_account(
49             termsOfServiceAgreed => 1,
50             );
51             }
52              
53             #Save $acme->key_id() somewhere so you can use it again.
54              
55             my $order = $acme->create_order(
56             identifiers => [
57             { type => 'dns', value => '*.example.com' },
58             ],
59             );
60              
61             my $authz = $acme->get_authorization( ($order->authorizations())[0] );
62              
63             my @challenges = $authz->challenges();
64              
65             # ... Pick a challenge, and satisfy it.
66              
67             $acme->accept_challenge($challenge);
68              
69             sleep 1 while 'valid' ne $acme->poll_authorization($authz);
70              
71             # ... Make a key and CSR for *.example.com
72              
73             $acme->finalize_order($order, $csr_pem_or_der);
74              
75             while ($order->status() ne 'valid') {
76             sleep 1;
77             $acme->poll_order($order);
78             }
79              
80             # ... and now fetch the certificate chain:
81              
82             my $pem_chain = $acme->get_certificate_chain($order);
83              
84             See F in the distribution for more fleshed-out examples.
85              
86             To use L, see
87             L.
88              
89             =head1 DESCRIPTION
90              
91             This library implements client logic for the
92             ACME (Automated Certificate Management Environment) protocol, as
93             standardized in L
94             and popularized by L.
95              
96             =head1 STATUS
97              
98             This is a production-grade implementation. While breaking changes at this
99             point are unlikely, please always check the changelog before upgrading to
100             a new version of this module.
101              
102             =head1 FEATURES
103              
104             =over
105              
106             =item * Support for both ECDSA and RSA encrytion.
107              
108             =item * Support for http-01, dns-01, and L challenges.
109              
110             =item * Comprehensive error handling with typed, L-based exceptions.
111              
112             =item * Supports blocking and (experimentally) non-blocking I/O.
113              
114             =item * L errors.|https://tools.ietf.org/html/rfc8555#section-6.5>
115              
116             =item * This is a pure-Perl solution. Most of its dependencies are
117             either core modules or pure Perl themselves. XS is necessary to
118             communicate with the ACME server via TLS; however, most Perl installations
119             already include the necessary logic (i.e., L) for TLS.
120              
121             In short, Net::ACME2 will run anywhere that Perl can speak TLS, which is
122             I everywhere that Perl runs.
123              
124             =back
125              
126             =head1 ERROR HANDLING
127              
128             All thrown exceptions are instances of L.
129             Specific error classes aren’t yet defined.
130              
131             =head1 CRYPTOGRAPHY & SPEED
132              
133             L provides all cryptographic operations that this library
134             needs using pure Perl. While this satisfies this module’s intent to be
135             as pure-Perl as possible, there are a couple of significant drawbacks
136             to this approach: firstly, it’s slower than XS-based code, and secondly,
137             it loses the security benefits of the vetting that more widely-used
138             cryptography libraries receive.
139              
140             To address these problems, Net::ACME2 will, after parsing a key, look
141             for and prefer the following XS-based libraries for cryptography instead:
142              
143             =over
144              
145             =item * L (based on L)
146              
147             =item * L (based on L)
148              
149             =back
150              
151             If the above are unavailable to you, then you may be able to speed up
152             your L installation; see that module’s documentation
153             for more details.
154              
155             =cut
156              
157             =head1 EXPERIMENTAL: NON-BLOCKING (ASYNCHRONOUS) I/O
158              
159             By default, Net::ACME2 uses blocking I/O.
160              
161             To facilitate asynchronous/non-blocking I/O, you may give an C
162             to C. This value must be an object that implements C.
163             That method should mimic L’s method of the same name
164             B that, instead of returning a hash reference, it should return
165             a promise. (à la L, L, L, etc.)
166             That promise’s resolution should be a single value that mimics
167             C’s return structure.
168              
169             When a Net::ACME2 instance is created with C, several of the
170             methods described below return promises. These promises resolve to the values
171             that otherwise would be returned directly in synchronous mode. Any exception
172             that would be thrown in synchronous mode is given as the promise’s rejection
173             value. This document’s convention to indicate a function that, in
174             asynchronous mode, returns a promise is:
175              
176             promise($whatever) = ...
177              
178             This distribution ships with L, a wrapper around
179             L, which in turns wraps L. This
180             provides out-of-the-box support for Perl’s most widely-used event interfaces;
181             see Net::Curl::Promiser’s documentation for more details.
182              
183             =cut
184              
185             #----------------------------------------------------------------------
186              
187 3     3   20 use Crypt::Format;
  3         8  
  3         86  
188 3     3   527 use MIME::Base64 ();
  3         681  
  3         90  
189              
190 3     3   1421 use Net::ACME2::AccountKey;
  3         8  
  3         98  
191              
192 3     3   1788 use Net::ACME2::HTTP;
  3         6  
  3         114  
193 3     3   1347 use Net::ACME2::Order;
  3         7  
  3         86  
194 3     3   1315 use Net::ACME2::Authorization;
  3         10  
  3         95  
195 3     3   26 use Net::ACME2::PromiseUtil;
  3         5  
  3         81  
196              
197             use constant {
198 3         212 _HTTP_OK => 200,
199             _HTTP_CREATED => 201,
200 3     3   15 };
  3         5  
201              
202             # accessed from test
203 3         151 use constant newAccount_booleans => qw(
204             termsOfServiceAgreed
205             onlyReturnExisting
206 3     3   17 );
  3         5  
207              
208             # the list of methods that need a “jwk” in their JWS Protected Header
209             # (cf. section 6.2 of the spec)
210 3         6233 use constant FULL_JWT_METHODS => qw(
211             newAccount
212             revokeCert
213 3     3   15 );
  3         6  
214              
215             #----------------------------------------------------------------------
216              
217             =head1 METHODS
218              
219             =head2 I->new( %OPTS )
220              
221             Instantiates an ACME2 object, which you’ll use for all
222             interactions with the ACME server. %OPTS is:
223              
224             =over
225              
226             =item * C - Required. The private key to associate with the ACME2
227             user. Anything that C can parse is acceptable.
228              
229             =item * C - Optional. As returned by C.
230             Saves a round-trip to the ACME2 server, so you should give this
231             if you have it.
232              
233             =item * C - Optional. A hash reference to use as the
234             directory contents. Saves a round-trip to the ACME2 server, but there’s
235             no built-in logic to determine when the cache goes invalid. Caveat
236             emptor.
237              
238             =item * C - Optional. Provides a custom UA object to facilitate
239             non-blocking I/O. This object B implement the interface described above.
240              
241             =back
242              
243             =cut
244              
245             sub new {
246 13     13 1 21732 my ( $class, %opts ) = @_;
247              
248 13 50       65 _die_generic('Need “key”!') if !$opts{'key'};
249              
250 13         86 return $class->_new_without_key_check(%opts);
251             }
252              
253             sub _new_without_key_check {
254 19     19   67 my ( $class, %opts ) = @_;
255              
256             my $self = {
257             _host => $class->HOST(%opts),
258             _key => $opts{'key'},
259             _key_id => $opts{'key_id'},
260             _directory => $opts{'directory'},
261 19         159 _async_ua => $opts{'async_ua'},
262             };
263              
264 19         61 bless $self, $class;
265              
266 19         88 $self->_set_http();
267              
268 19         108 return $self;
269             }
270              
271             #----------------------------------------------------------------------
272              
273             =head2 $id = I->key_id()
274              
275             Returns the object’s cached key ID, either as given at instantiation
276             or as fetched in C.
277              
278             =cut
279              
280             sub key_id {
281 24     24 1 28866 my ($self) = @_;
282              
283 24         98 return $self->{'_key_id'};
284             }
285              
286             #----------------------------------------------------------------------
287              
288             =head2 I->http_timeout( [$NEW] )
289              
290             A passthrough interface to the underlying L object’s
291             C method.
292              
293             Throws an exception if C was given to C.
294              
295             =cut
296              
297             sub http_timeout {
298 0     0 1 0 my $self = shift;
299              
300 0 0       0 die 'Don’t call in asynchronous mode!' if $self->{'_async_ua'};
301              
302 0         0 return $self->{'_http'}->timeout(@_);
303             }
304              
305             #----------------------------------------------------------------------
306              
307             =head2 promise($url) = I->get_terms_of_service()
308              
309             Returns the URL for the terms of service. Callable as either
310             a class method or an instance method.
311              
312             =cut
313              
314             sub get_terms_of_service {
315 18     18 1 14266 my ($self) = @_;
316              
317             # We want to be able to call this as a class method.
318 18 100       72 if (!ref $self) {
319 6         34 $self = $self->_new_without_key_check();
320             }
321              
322             return Net::ACME2::PromiseUtil::then(
323             $self->_get_directory(),
324             sub {
325 18     18   36 my $dir = shift;
326              
327             # Exceptions here indicate an ACME violation and should be
328             # practically nonexistent.
329 18 50       60 my $url = $dir->{'meta'} or _die_generic('No “meta” in directory!');
330 18 50       54 $url = $url->{'termsOfService'} or _die_generic('No “termsOfService” in directory metadata!');
331              
332 18         112 return $url;
333             },
334 18         78 );
335             }
336              
337             #----------------------------------------------------------------------
338              
339             =head2 promise($created_yn) = I->create_account( %OPTS )
340              
341             Creates an account using the ACME2 object’s key and the passed
342             %OPTS, which are as described in the ACME2 spec (cf. C).
343             Boolean values may be given as simple Perl booleans.
344              
345             Returns 1 if the account is newly created
346             or 0 if the account already existed.
347              
348             NB: C is an alias for this method.
349              
350             =cut
351              
352             sub create_account {
353 24     24 1 13590 my ($self, %opts) = @_;
354              
355 24         104 for my $name (newAccount_booleans()) {
356 48 100       536 next if !exists $opts{$name};
357 12   33     91 ($opts{$name} &&= JSON::true()) ||= JSON::false();
      33        
358             }
359              
360             return Net::ACME2::PromiseUtil::then(
361             $self->_post( 'newAccount', \%opts ),
362             sub {
363 24     24   72 my ($resp) = @_;
364              
365 24         96 $self->{'_key_id'} = $resp->header('location');
366              
367 24         354 $self->{'_http'}->set_key_id( $self->{'_key_id'} );
368              
369 24 100       556 return 0 if $resp->status() == _HTTP_OK;
370              
371 12 50       276 $resp->die_because_unexpected() if $resp->status() != _HTTP_CREATED;
372              
373 12         158 my $struct = $resp->content_struct();
374              
375 12 50       199 if ($struct) {
376 12         50 for my $name (newAccount_booleans()) {
377 24 100       234 next if !exists $struct->{$name};
378 12   50     97 ($struct->{$name} &&= 1) ||= 0;
      50        
379             }
380             }
381              
382 12         142 return 1;
383             },
384 24         148 );
385             }
386              
387             #----------------------------------------------------------------------
388              
389             =head2 promise($order) = I->create_order( %OPTS )
390              
391             Returns a L object. %OPTS is as described in the
392             ACME spec (cf. C). Boolean values may be given as simple
393             Perl booleans.
394              
395             NB: C is an alias for this method.
396              
397             =cut
398              
399             sub create_order {
400 0     0 1 0 my ($self, %opts) = @_;
401              
402 0         0 $self->_require_key_id(\%opts);
403              
404             return Net::ACME2::PromiseUtil::then(
405             $self->_post( 'newOrder', \%opts ),
406             sub {
407 0     0   0 my ($resp) = @_;
408              
409 0 0       0 $resp->die_because_unexpected() if $resp->status() != _HTTP_CREATED;
410              
411             return Net::ACME2::Order->new(
412             id => $resp->header('location'),
413 0         0 %{ $resp->content_struct() },
  0         0  
414             );
415             },
416 0         0 );
417             }
418              
419             #----------------------------------------------------------------------
420              
421             =head2 promise($authz) = I->get_authorization( $URL )
422              
423             Fetches the authorization’s information based on the given $URL
424             and returns a L object.
425              
426             The URL is as given by L’s C method.
427              
428             =cut
429              
430             sub get_authorization {
431 0     0 1 0 my ($self, $id) = @_;
432              
433             return Net::ACME2::PromiseUtil::then(
434             $self->_post_as_get($id),
435             sub {
436 0     0   0 my $resp = shift;
437              
438             return Net::ACME2::Authorization->new(
439             id => $id,
440 0         0 %{ $resp->content_struct() },
  0         0  
441             );
442             },
443 0         0 );
444             }
445              
446             #----------------------------------------------------------------------
447              
448             =head2 $str = I->make_key_authorization( $CHALLENGE )
449              
450             Accepts an instance of L (probably a subclass
451             thereof) and returns
452             a key authorization string suitable for handling the given $CHALLENGE.
453             See F in the distribution for example usage.
454              
455             If you’re using HTTP authorization and are on the same server as the
456             domains’ document roots, then look at the handler logic in
457             L for a potentially simpler way to
458             handle HTTP challenges.
459              
460             =cut
461              
462             sub make_key_authorization {
463 2     2 1 15 my ($self, $challenge_obj) = @_;
464              
465 2 50       9 _die_generic('Need a challenge object!') if !$challenge_obj;
466              
467 2         22 return $challenge_obj->token() . '.' . $self->_key_thumbprint();
468             }
469              
470             #----------------------------------------------------------------------
471              
472             =head2 promise() = I->accept_challenge( $CHALLENGE )
473              
474             Signal to the ACME server that the CHALLENGE is ready.
475              
476             =cut
477              
478             sub accept_challenge {
479 0     0 1 0 my ($self, $challenge_obj) = @_;
480              
481             return Net::ACME2::PromiseUtil::then(
482             $self->_post_url(
483             $challenge_obj->url(),
484             {
485             keyAuthorization => $self->make_key_authorization($challenge_obj),
486             },
487             ),
488 0     0   0 sub { undef },
489 0         0 );
490             }
491              
492             #----------------------------------------------------------------------
493              
494             =head2 promise($status) = I->poll_authorization( $AUTHORIZATION )
495              
496             Accepts a L instance and polls the
497             ACME server for that authorization’s status. The $AUTHORIZATION
498             object is then updated with the results of the poll.
499              
500             As a courtesy, this returns the $AUTHORIZATION’s new C.
501              
502             =cut
503              
504             #This has to handle updates to the authz and challenge objects
505             *poll_authorization = *_poll_order_or_authz;
506              
507             #----------------------------------------------------------------------
508              
509             =head2 promise($status) = I->finalize_order( $ORDER, $CSR )
510              
511             Finalizes an order and updates the $ORDER object with the returned
512             status. $CSR may be in either DER or PEM format.
513              
514             As a courtesy, this returns the $ORDER’s C. If this does
515             not equal C, then you should probably C
516             until it does.
517              
518             =cut
519              
520             sub finalize_order {
521 0     0 1 0 my ($self, $order_obj, $csr) = @_;
522              
523 0         0 my $csr_der;
524 0 0       0 if (index($csr, '-----') == 0) {
525 0         0 $csr_der = Crypt::Format::pem2der($csr);
526             }
527             else {
528 0         0 $csr_der = $csr;
529             }
530              
531 0         0 $csr = MIME::Base64::encode_base64url($csr_der);
532              
533             return Net::ACME2::PromiseUtil::then(
534             $self->_post_url(
535             $order_obj->finalize(),
536             {
537             csr => $csr,
538             },
539             ),
540             sub {
541 0     0   0 my $post = shift;
542              
543 0         0 my $content = $post->content_struct();
544              
545 0         0 $order_obj->update($content);
546              
547 0         0 return $order_obj->status();
548             },
549 0         0 );
550             }
551              
552             #----------------------------------------------------------------------
553              
554             =head2 promise($status) = I->poll_order( $ORDER )
555              
556             Like C but handles a
557             L object instead.
558              
559             =cut
560              
561             *poll_order = *_poll_order_or_authz;
562              
563             #----------------------------------------------------------------------
564              
565             =head2 promise($cert) = I->get_certificate_chain( $ORDER )
566              
567             Fetches the $ORDER’s certificate chain and returns
568             it in the format implied by the
569             C MIME type. See the ACME
570             protocol specification for details about this format.
571              
572             =cut
573              
574             sub get_certificate_chain {
575 0     0 1 0 my ($self, $order) = @_;
576              
577             return Net::ACME2::PromiseUtil::then(
578             $self->_post_as_get( $order->certificate() ),
579             sub {
580 0     0   0 return shift()->content();
581             },
582 0         0 );
583             }
584              
585             #----------------------------------------------------------------------
586              
587             sub _key_thumbprint {
588 2     2   6 my ($self) = @_;
589              
590 2   66     17 return $self->{'_key_thumbprint'} ||= $self->_key_obj()->get_jwk_thumbprint();
591             }
592              
593             sub _get_directory {
594 66     66   130 my ($self) = @_;
595              
596 66   66     461 return $self->{'_directory_promise'} ||= do {
597 18         72 my $dir_path = $self->DIRECTORY_PATH();
598              
599 18         37 my $http = $self->{'_http'};
600              
601             Net::ACME2::PromiseUtil::then(
602             $self->{'_http'}->get("https://$self->{'_host'}$dir_path"),
603             sub {
604 18     18   82 my $dir_hr = shift()->content_struct();
605              
606 18 50       323 my $new_nonce_url = $dir_hr->{'newNonce'} or do {
607 0         0 _die_generic('Directory lacks “newNonce”.');
608             };
609              
610 18         78 $http->set_new_nonce_url( $new_nonce_url );
611              
612 18         142 return $dir_hr;
613             },
614 18         103 );
615             };
616             }
617              
618             sub _require_key_id {
619 0     0   0 my ($self, $opts_hr) = @_;
620              
621 0 0       0 $opts_hr->{'_key_id'} = $self->{'_key_id'} or do {
622 0         0 _die_generic('No key ID has been set. Either pass “key_id” to new(), or create_account().');
623             };
624              
625             return
626 0         0 }
627              
628             sub _poll_order_or_authz {
629 0     0   0 my ($self, $order_or_authz_obj) = @_;
630              
631             return Net::ACME2::PromiseUtil::then(
632             $self->_post_as_get( $order_or_authz_obj->id() ),
633             sub {
634 0     0   0 my $get = shift;
635              
636 0         0 my $content = $get->content_struct();
637              
638 0         0 $order_or_authz_obj->update($content);
639              
640 0         0 return $order_or_authz_obj->status();
641             },
642 0         0 );
643             }
644              
645             sub _key_obj {
646 14     14   39 my ($self) = @_;
647              
648 14   66     149 return $self->{'_key_obj'} ||= Net::ACME2::AccountKey->new($self->{'_key'});
649             }
650              
651             sub _set_http {
652 19     19   55 my ($self) = @_;
653              
654             $self->{'_http'} = Net::ACME2::HTTP->new(
655             key => $self->{'_key'} && $self->_key_obj(),
656             key_id => $self->{'_key_id'},
657 19   66     152 ua => $self->{'_async_ua'},
658             );
659              
660 19         50 return;
661             }
662              
663             our $_POST_METHOD;
664              
665             sub _post {
666 24     24   88 my ( $self, $link_name, $data ) = @_;
667              
668 24         39 my $post_method;
669 24 50       78 $post_method = 'post_full_jwt' if grep { $link_name eq $_ } FULL_JWT_METHODS();
  48         186  
670              
671             return Net::ACME2::PromiseUtil::then(
672             $self->_get_directory(),
673             sub {
674 24     24   64 my $dir_hr = shift;
675              
676             # Since the $link_name will come from elsewhere in this module
677             # there really shouldn’t be an error here, but just in case.
678 24 50       95 my $url = $dir_hr->{$link_name} or _die_generic("Unknown link name: “$link_name”");
679              
680 24         93 return $self->_post_url( $url, $data, $post_method );
681             },
682 24         113 );
683             }
684              
685             sub _post_as_get {
686 0     0   0 my ( $self, $url ) = @_;
687              
688 0         0 return $self->_post_url( $url, q<> );
689             }
690              
691             sub _post_url {
692 24     24   71 my ( $self, $url, $data, $opt_post_method ) = @_;
693              
694 24   50     71 my $post_method = $opt_post_method || 'post_key_id';
695              
696 24         122 my $http = $self->{'_http'};
697              
698             #Do this in case we haven’t initialized the directory yet.
699             #Initializing the directory is necessary to get a nonce.
700             return Net::ACME2::PromiseUtil::then(
701             $self->_get_directory(),
702             sub {
703 24     24   114 return $http->$post_method( $url, $data );
704             },
705 24         128 );
706             }
707              
708             sub _die_generic {
709 0     0     die Net::ACME2::X->create('Generic', @_);
710             }
711              
712             #legacy aliases
713             *create_new_account = *create_account;
714             *create_new_order = *create_order;
715              
716             # sub DESTROY {
717             # print "ACME2 destroyed at ${^GLOBAL_PHASE}\n";
718             # }
719              
720             1;
721              
722             =head1 TODO
723              
724             =over
725              
726             =item * Add pre-authorization support if there is ever a production
727             use for it.
728              
729             =item * Expose the Retry-After header via the module API.
730              
731             =item * There is currently no way to fetch an order or challenge’s
732             properties via URL. Prior to ACME’s adoption of “POST-as-GET” this was
733             doable via a plain GET to the URL, but that’s no longer possible.
734             If there’s a need, I’ll consider adding such logic to Net::ACME2.
735             (It’s trivial to add; I’d just like to keep things as
736             simple as possible.)
737              
738             =item * Add (more) tests.
739              
740             =back
741              
742             =head1 SEE ALSO
743              
744             L is another ACME client library.
745              
746             L provides this library’s default cryptography backend.
747             See this distribution’s F directory for sample usage
748             to generate keys and CSRs.
749              
750             L implements client logic for the variant of this
751             protocol that Let’s Encrypt first deployed.
752              
753             =cut