File Coverage

blib/lib/Net/ACME2.pm
Criterion Covered Total %
statement 89 124 71.7
branch 16 32 50.0
condition 11 21 52.3
subroutine 22 29 75.8
pod 9 9 100.0
total 147 215 68.3


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