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