File Coverage

blib/lib/Net/ACME2.pm
Criterion Covered Total %
statement 89 128 69.5
branch 16 32 50.0
condition 11 21 52.3
subroutine 22 31 70.9
pod 10 10 100.0
total 148 222 66.6


line stmt bran cond sub pod time code
1             package Net::ACME2;
2              
3 3     3   480 use strict;
  3         6  
  3         88  
4 3     3   15 use warnings;
  3         6  
  3         119  
5              
6             =encoding utf-8
7              
8             =head1 NAME
9              
10             Net::ACME2 - Client logic for the ACME (Let’s Encrypt) protocol
11              
12             X X X
13              
14             =head1 SYNOPSIS
15              
16             package SomeCA::ACME;
17              
18             use parent qw( Net::ACME2 );
19              
20             use constant {
21             DIRECTORY_PATH => '/acme-directory',
22             };
23              
24             # %opts are the parameters given to new().
25             sub HOST {
26             my ($class, %opts) = @_;
27              
28             # You can make this depend on the %opts if you want.
29             return 'acme.someca.net';
30             }
31              
32             package main;
33              
34             my $acme = SomeCA::ACME->new(
35             key => $account_key_pem_or_der,
36             key_id => undef,
37             );
38              
39             #for a new account
40             {
41             my $terms_url = $acme->get_terms_of_service();
42              
43             $acme->create_account(
44             termsOfServiceAgreed => 1,
45             );
46             }
47              
48             #Save $acme->key_id() somewhere so you can use it again.
49              
50             my $order = $acme->create_order(
51             identifiers => [
52             { type => 'dns', value => '*.example.com' },
53             ],
54             );
55              
56             my $authz = $acme->get_authorization( ($order->authorizations())[0] );
57              
58             my @challenges = $authz->challenges();
59              
60             # ... Pick a challenge, and satisfy it.
61              
62             $acme->accept_challenge($challenge);
63              
64             sleep 1 while 'valid' ne $acme->poll_authorization($authz);
65              
66             # ... Make a key and CSR for *.example.com
67              
68             $acme->finalize_order($order, $csr_pem_or_der);
69              
70             while ($order->status() ne 'valid') {
71             sleep 1;
72             $acme->poll_order($order);
73             }
74              
75             # ... and now fetch the certificate chain:
76              
77             my $pem_chain = $acme->get_certificate_chain($order);
78              
79             See F in the distribution for more fleshed-out examples.
80              
81             To use L, see
82             L.
83              
84             =head1 DESCRIPTION
85              
86             This library implements client logic for the
87             ACME (Automated Certificate Management Environment) protocol, as
88             standardized in L
89             and popularized by L.
90              
91             =head1 STATUS
92              
93             This is a production-grade implementation. While breaking changes at this
94             point are unlikely, please always check the changelog before upgrading to
95             a new version of this module.
96              
97             =head1 FEATURES
98              
99             =over
100              
101             =item * Support for both ECDSA and RSA encrytion.
102              
103             =item * Support for http-01, dns-01, and L challenges.
104              
105             =item * Comprehensive error handling with typed, L-based exceptions.
106              
107             =item * This is a pure-Perl solution. Most of its dependencies are
108             either core modules or pure Perl themselves. XS is necessary to
109             communicate with the ACME server via TLS; however, most Perl installations
110             already include the necessary logic (i.e., L) for TLS.
111              
112             In short, Net::ACME2 will run anywhere that Perl can speak TLS, which is
113             I everywhere that Perl runs.
114              
115             =back
116              
117             =head1 ERROR HANDLING
118              
119             All thrown exceptions are instances of L.
120             Specific error classes aren’t yet defined.
121              
122             =head1 SPEED
123              
124             If you notice speed problems, check to see if your L
125             installation can be made faster.
126              
127             =cut
128              
129 3     3   15 use Crypt::Format;
  3         6  
  3         68  
130 3     3   468 use MIME::Base64 ();
  3         666  
  3         57  
131              
132 3     3   1276 use Net::ACME2::AccountKey;
  3         10  
  3         91  
133              
134 3     3   1270 use Net::ACME2::HTTP;
  3         8  
  3         104  
135 3     3   1251 use Net::ACME2::Order;
  3         9  
  3         90  
136 3     3   1201 use Net::ACME2::Authorization;
  3         9  
  3         138  
137              
138             our $VERSION = '0.27-TRIAL1';
139              
140             use constant {
141 3         188 _HTTP_OK => 200,
142             _HTTP_CREATED => 201,
143 3     3   19 };
  3         6  
144              
145             # accessed from test
146 3         150 use constant newAccount_booleans => qw(
147             termsOfServiceAgreed
148             onlyReturnExisting
149 3     3   18 );
  3         6  
150              
151             # the list of methods that need a “jwk” in their JWS Protected Header
152             # (cf. section 6.2 of the spec)
153 3         4769 use constant FULL_JWT_METHODS => qw(
154             newAccount
155             revokeCert
156 3     3   16 );
  3         6  
157              
158             =head1 METHODS
159              
160             =head2 I->new( %OPTS )
161              
162             Instantiates an ACME2 object, which you’ll use for all
163             interactions with the ACME server. %OPTS is:
164              
165             =over
166              
167             =item * C - Required. The private key to associate with the ACME2
168             user. Anything that C can parse is acceptable.
169              
170             =item * C - Optional. As returned by C.
171             Saves a round-trip to the ACME2 server, so you should give this
172             if you have it.
173              
174             =item * C - Optional. A hash reference to use as the
175             directory contents. Saves a round-trip to the ACME2 server, but there’s
176             no built-in logic to determine when the cache goes invalid. Caveat
177             emptor.
178              
179             =back
180              
181             =cut
182              
183             sub new {
184 13     13 1 16838 my ( $class, %opts ) = @_;
185              
186 13 50       62 _die_generic('Need “key”!') if !$opts{'key'};
187              
188             my $self = {
189             _host => $class->HOST(%opts),
190             _key => $opts{'key'},
191             _key_id => $opts{'key_id'},
192 13         106 _directory => $opts{'directory'},
193             };
194              
195 13         39 bless $self, $class;
196              
197 13         70 $self->_set_ua();
198              
199 13         83 return $self;
200             }
201              
202             =head2 $id = I->key_id()
203              
204             Returns the object’s cached key ID, either as given at instantiation
205             or as fetched in C.
206              
207             =cut
208              
209             sub key_id {
210 24     24 1 23752 my ($self) = @_;
211              
212 24         96 return $self->{'_key_id'};
213             }
214              
215             =head2 $url = I->get_terms_of_service()
216              
217             Returns the URL for the terms of service.
218              
219             B For L you can
220             unofficially resolve against
221             L to see the terms
222             of service.
223              
224             =cut
225              
226             sub get_terms_of_service {
227 12     12 1 8463 my ($self) = @_;
228              
229             #We want to be able to call this as a class method.
230 12 50       56 if (!ref $self) {
231 0         0 $self = $self->new();
232             }
233              
234 12         59 my $dir = $self->_get_directory();
235              
236             # Exceptions here indicate an ACME violation and should be
237             # practically nonexistent.
238 12 50       46 my $url = $dir->{'meta'} or _die_generic('No “meta” in directory!');
239 12 50       43 $url = $url->{'termsOfService'} or _die_generic('No “termsOfService” in directory metadata!');
240              
241 12         45 return $url;
242             }
243              
244             =head2 $created_yn = I->create_account( %OPTS )
245              
246             Creates an account using the ACME2 object’s key and the passed
247             %OPTS, which are as described in the ACME2 spec (cf. C).
248             Boolean values may be given as simple Perl booleans.
249              
250             Returns 1 if the account is newly created
251             or 0 if the account already existed.
252              
253             NB: C is an alias for this method.
254              
255             =cut
256              
257             sub create_account {
258 24     24 1 12312 my ($self, %opts) = @_;
259              
260 24         92 for my $name (newAccount_booleans()) {
261 48 100       472 next if !exists $opts{$name};
262 12   33     91 ($opts{$name} &&= JSON::true()) ||= JSON::false();
      33        
263             }
264              
265 24         117 my $resp = $self->_post(
266             'newAccount',
267             \%opts,
268             );
269              
270 24         106 $self->{'_key_id'} = $resp->header('location');
271              
272 24         312 $self->{'_ua'}->set_key_id( $self->{'_key_id'} );
273              
274 24 100       512 return 0 if $resp->status() == _HTTP_OK;
275              
276 12 50       241 $resp->die_because_unexpected() if $resp->status() != _HTTP_CREATED;
277              
278 12         128 my $struct = $resp->content_struct();
279              
280 12 50       155 if ($struct) {
281 12         44 for my $name (newAccount_booleans()) {
282 24 100       219 next if !exists $struct->{$name};
283 12   50     104 ($struct->{$name} &&= 1) ||= 0;
      50        
284             }
285             }
286              
287 12         57 return 1;
288             }
289              
290             =head2 $order = I->create_order( %OPTS )
291              
292             Returns a L object. %OPTS is as described in the
293             ACME spec (cf. C). Boolean values may be given as simple
294             Perl booleans.
295              
296             NB: C is an alias for this method.
297              
298             =cut
299              
300             sub create_order {
301 0     0 1 0 my ($self, %opts) = @_;
302              
303 0         0 $self->_require_key_id(\%opts);
304              
305 0         0 my $resp = $self->_post( 'newOrder', \%opts );
306              
307 0 0       0 $resp->die_because_unexpected() if $resp->status() != _HTTP_CREATED;
308              
309             return Net::ACME2::Order->new(
310             id => $resp->header('location'),
311 0         0 %{ $resp->content_struct() },
  0         0  
312             );
313             }
314              
315             =head2 $authz = I->get_authorization( $URL )
316              
317             Fetches the authorization’s information based on the given $URL
318             and returns a L object.
319              
320             The URL is as given by L’s C method.
321              
322             =cut
323              
324             sub get_authorization {
325 0     0 1 0 my ($self, $id) = @_;
326              
327 0         0 my $resp = $self->_post_as_get($id);
328              
329             return Net::ACME2::Authorization->new(
330             id => $id,
331 0         0 %{ $resp->content_struct() },
  0         0  
332             );
333             }
334              
335             =head2 $str = I->make_key_authorization( $CHALLENGE )
336              
337             Accepts an instance of L (probably a subclass
338             thereof) and returns
339             a key authorization string suitable for handling the given $CHALLENGE.
340             See F in the distribution for example usage.
341              
342             If you’re using HTTP authorization and are on the same server as the
343             domains’ document roots, then look at the handler logic in
344             L for a potentially simpler way to
345             handle HTTP challenges.
346              
347             =cut
348              
349             sub make_key_authorization {
350 2     2 1 29 my ($self, $challenge_obj) = @_;
351              
352 2 50       12 _die_generic('Need a challenge object!') if !$challenge_obj;
353              
354 2         24 return $challenge_obj->token() . '.' . $self->_key_thumbprint();
355             }
356              
357             =head2 I->accept_challenge( $CHALLENGE )
358              
359             Signal to the ACME server that the CHALLENGE is ready.
360              
361             =cut
362              
363             sub accept_challenge {
364 0     0 1 0 my ($self, $challenge_obj) = @_;
365              
366 0         0 $self->_post_url(
367             $challenge_obj->url(),
368             {
369             keyAuthorization => $self->make_key_authorization($challenge_obj),
370             },
371             );
372              
373 0         0 return;
374             }
375              
376             =head2 $status = I->poll_authorization( $AUTHORIZATION )
377              
378             Accepts a L instance and polls the
379             ACME server for that authorization’s status. The $AUTHORIZATION
380             object is then updated with the results of the poll.
381              
382             As a courtesy, this returns the $AUTHORIZATION’s new C.
383              
384             =cut
385              
386             #This has to handle updates to the authz and challenge objects
387             *poll_authorization = *_poll_order_or_authz;
388              
389             =head2 $status = I->finalize_order( $ORDER, $CSR )
390              
391             Finalizes an order and updates the $ORDER object with the returned
392             status. $CSR may be in either DER or PEM format.
393              
394             As a courtesy, this returns the $ORDER’s C. If this does
395             not equal C, then you should probably C
396             until it does.
397              
398             =cut
399              
400             sub finalize_order {
401 0     0 1 0 my ($self, $order_obj, $csr) = @_;
402              
403 0         0 my $csr_der;
404 0 0       0 if (index($csr, '-----') == 0) {
405 0         0 $csr_der = Crypt::Format::pem2der($csr);
406             }
407             else {
408 0         0 $csr_der = $csr;
409             }
410              
411 0         0 $csr = MIME::Base64::encode_base64url($csr_der);
412              
413 0         0 my $post = $self->_post_url(
414             $order_obj->finalize(),
415             {
416             csr => $csr,
417             },
418             );
419              
420 0         0 my $content = $post->content_struct();
421              
422 0         0 $order_obj->update($content);
423              
424 0         0 return $order_obj->status();
425             }
426              
427             =head2 $status = I->poll_order( $ORDER )
428              
429             Like C but handles a
430             L object instead.
431              
432             =cut
433              
434             *poll_order = *_poll_order_or_authz;
435              
436             =head2 $cert = I->get_certificate_chain( $ORDER )
437              
438             Fetches the $ORDER’s certificate chain and returns
439             it in the format implied by the
440             C MIME type. See the ACME
441             protocol specification for details about this format.
442              
443             =cut
444              
445             sub get_certificate_chain {
446 0     0 1 0 my ($self, $order) = @_;
447              
448 0         0 return $self->_post_as_get( $order->certificate() )->content();
449             }
450              
451             #----------------------------------------------------------------------
452              
453             sub _key_thumbprint {
454 2     2   8 my ($self) = @_;
455              
456 2   66     16 return $self->{'_key_thumbprint'} ||= $self->_key_obj()->get_jwk_thumbprint();
457             }
458              
459             sub _get_directory {
460 60     60   108 my ($self) = @_;
461              
462 60   66     170 $self->{'_directory'} ||= do {
463 12         54 my $dir_path = $self->DIRECTORY_PATH();
464 12         97 $self->{'_ua'}->get("https://$self->{'_host'}$dir_path")->content_struct();
465             };
466              
467 60 50       362 my $new_nonce_url = $self->{'_directory'}{'newNonce'} or do {
468 0         0 _die_generic('Directory is missing “newNonce”.');
469             };
470              
471 60         422 $self->{'_ua'}->set_new_nonce_url( $new_nonce_url );
472              
473 60         152 return $self->{'_directory'};
474             }
475              
476             sub _require_key_id {
477 0     0   0 my ($self, $opts_hr) = @_;
478              
479 0 0       0 $opts_hr->{'_key_id'} = $self->{'_key_id'} or do {
480 0         0 _die_generic('No key ID has been set. Either pass “key_id” to new(), or create_account().');
481             };
482              
483             return
484 0         0 }
485              
486             sub _poll_order_or_authz {
487 0     0   0 my ($self, $order_or_authz_obj) = @_;
488              
489 0         0 my $get = $self->_post_as_get( $order_or_authz_obj->id() );
490              
491 0         0 my $content = $get->content_struct();
492              
493 0         0 $order_or_authz_obj->update($content);
494              
495 0         0 return $order_or_authz_obj->status();
496             }
497              
498             sub _key_obj {
499 14     14   41 my ($self) = @_;
500              
501 14   66     160 return $self->{'_key_obj'} ||= Net::ACME2::AccountKey->new($self->{'_key'});
502             }
503              
504             sub _set_ua {
505 13     13   40 my ($self) = @_;
506              
507             $self->{'_ua'} = Net::ACME2::HTTP->new(
508             key => $self->_key_obj(),
509 13         76 key_id => $self->{'_key_id'},
510             );
511              
512 13         41 return;
513             }
514              
515             our $_POST_METHOD;
516              
517             sub _post {
518 24     24   72 my ( $self, $link_name, $data ) = @_;
519              
520 24         45 my $post_method;
521 24 50       80 $post_method = 'post_full_jwt' if grep { $link_name eq $_ } FULL_JWT_METHODS();
  48         160  
522              
523             # Since the $link_name will come from elsewhere in this module
524             # there really shouldn’t be an error here, but just in case.
525 24 50       93 my $url = $self->_get_directory()->{$link_name} or _die_generic("Unknown link name: “$link_name”");
526              
527 24         104 return $self->_post_url( $url, $data, $post_method );
528             }
529              
530             sub _post_as_get {
531 0     0   0 my ( $self, $url ) = @_;
532              
533 0         0 return $self->_post_url( $url, q<> );
534             }
535              
536             sub _post_url {
537 24     24   80 my ( $self, $url, $data, $opt_post_method ) = @_;
538              
539             #Do this in case we haven’t initialized the directory yet.
540             #Initializing the directory is necessary to get a nonce.
541 24         62 $self->_get_directory();
542              
543 24   50     92 my $post_method = $opt_post_method || 'post_key_id';
544              
545 24         98 return $self->{'_ua'}->$post_method( $url, $data );
546             }
547              
548             sub _die_generic {
549 0     0     die Net::ACME2::X->create('Generic', @_);
550             }
551              
552             #legacy aliases
553             *create_new_account = \*create_account;
554             *create_new_order = \*create_order;
555              
556             1;
557              
558             =head1 TODO
559              
560             =over
561              
562             =item * Add pre-authorization support if there is ever a production
563             use for it.
564              
565             =item * Expose the Retry-After header via the module API.
566              
567             =item * There is currently no way to fetch an order or challenge’s
568             properties via URL. Prior to ACME’s adoption of “POST-as-GET” this was
569             doable via a plain GET to the URL, but that’s no longer possible.
570             If there’s a need, I’ll consider adding such logic to Net::ACME2.
571             (It’s trivial to add; I’d just like to keep things as
572             simple as possible.)
573              
574             =item * Add (more) tests.
575              
576             =back
577              
578             =head1 SEE ALSO
579              
580             L provides this library’s cryptography backend. See
581             this distribution’s F directory for sample usage
582             to generate keys and CSRs.
583              
584             L implements client logic for the variant of this
585             protocol that Let’s Encrypt first deployed.
586              
587             =cut