File Coverage

blib/lib/Protocol/ACME.pm
Criterion Covered Total %
statement 199 341 58.3
branch 55 126 43.6
condition 4 10 40.0
subroutine 30 40 75.0
pod 14 15 93.3
total 302 532 56.7


line stmt bran cond sub pod time code
1             package Protocol::ACME;
2              
3 6     6   195485 use 5.007003;
  6         13  
4 6     6   18 use strict;
  6         7  
  6         101  
5 6     6   18 use warnings;
  6         5  
  6         513  
6              
7              
8             our $VERSION = '1.01';
9              
10             =head1 NAME
11              
12             Protocol::ACME - Interface to the Let's Encrypt ACME API
13              
14             =head1 VERSION
15              
16             Version 1.01
17              
18             =head1 SYNOPSIS
19              
20             use Protocol::ACME;
21              
22             my @names = qw( www.example.com cloud.example.com );
23              
24             my $challenges = {
25             'www.example.com' => Protocol::ACME::Challenge::SimpleSSH->new(
26             { ssh_host => "host1", www_root => "~/www" }
27             ),
28             'cloud.example.com' => Protocol::ACME::Challenge::SimpleSSH->new(
29             { ssh_host => "home2", www_root => "/opt/local/www/htdocs" }
30             )
31             };
32              
33             eval
34             {
35             my $acme = Protocol::ACME->new( host => $host,
36             account_key => $account_key_pem_or_der,
37             );
38              
39             $acme->directory();
40             $acme->register();
41             $acme->accept_tos();
42              
43             for my $domain ( @names )
44             {
45             $acme->authz( $domain );
46             $acme->handle_challenge( $challenges->{$domain} );
47             $acme->check_challenge();
48             $acme->cleanup_challenge( $challenges->{$domain} );
49             }
50              
51             my $cert = $acme->sign( $csr_file );
52             };
53             if ( $@ )
54             {
55             die if !UNIVERSAL::isa($@, 'Protocol::ACME::Exception');
56             die "Error occurred: Status: $@->{status},
57             Detail: $@->{detail},
58             Type: $@->{type}\n";
59             }
60             else
61             {
62             # do something appropriate with the DER encoded cert
63             print "Success\n";
64             }
65              
66             =head1 DESCRIPTION
67              
68             The C is a class implementing an interface for the
69             Let's Encrypt ACME API.
70              
71             The class handles the protocol details behind provisioning a Let's
72             Encrypt certificate.
73              
74             =head1 CONSTRUCTOR METHODS
75              
76             The following constructor methods are available:
77              
78             =over 4
79              
80             =item $acme = Protcol::ACME->new( %options )
81              
82             This method constructs a new C object and returns it.
83             Key/value pair arguments may be provided to set up the initial state.
84             The may be passed in as a hash or a hashref. The following options
85             correspond to attribute methods described below. Items marked with
86             a * are required.
87              
88             KEY DEFAULT
89             ----------- --------------------
90             *host undef
91             account_key undef
92             openssl undef
93             ua HTTP::Tiny->new()
94             loglevel error
95             debug 0
96             mailto undef
97              
98             B: The API end point to connect to. This will generally be acme-staging.api.letsencrypt.org
99             or acme-v01.api.letsencrypt.org
100              
101             B: The account private key in a scalar ref or filename. See C<$self->account_key>
102             for details on this arguemtn.
103              
104             B: The path to openssl. If this option is used a local version of the openssl binary will
105             be used for crypto operations rather than C.
106              
107             B: An HTTP::Tiny object customized as you see fit
108              
109             B: Set the loglevel to one of the C values.
110              
111             B: If set to non-zero this is a shortcut for C debug>
112              
113             B: This should be the email address that you want associated with your account. This is used
114             my Let's Encrypt for expiration notification.
115              
116             =back
117              
118             =head2 METHODS
119              
120             =over
121              
122             =item account_key( $key_filename )
123              
124             =item account_key( \$buffer )
125              
126             =item account_key( \%explicit_args )
127              
128              
129             C will load a the private account key if it was not already loaded
130             when the C object was constructed. There are three ways to call this:
131              
132             If the arg is a B it is assumed to be the filename of the
133             key. C will throw an error if there are problems reading the file.
134              
135             If the arg is a B reference it is assumed to be a buffer that
136             contains the KEY.
137              
138             If the arg is a B reference it contains named arguments. The arguments
139             are:
140              
141             KEY DEFAUL DESC
142             ----------- ----------- -------------------
143             filename undef The key Filename
144             buffer undef Buffer containing the key
145             format undef Explicitly state the format ( DER | PEM )
146              
147             If both C and C are set the C argument will be ignored.
148              
149             If the format is not explcitly set C will look at the key and
150             try and determine what the format it.
151              
152              
153             =item load_key_from_disk( $key_path )
154              
155             B
156              
157             Load a key from disk. Currently the key needs to be unencrypted.
158             Callbacks for handling password protected keys are still to come.
159              
160             =item directory()
161              
162             Loads the directory from the ACME host. This call must be made first
163             before any other calls to the API in order the bootstrap the API
164             resource list.
165              
166             =item register( %args )
167              
168             Call the new-reg resource and create an account associated with the
169             loaded account key. If that key has already been registered this method
170             will gracefully and silently handle that.
171              
172             Arguments that can be passed in:
173              
174             KEY DEFAULT
175             ----------- --------------------
176             mailto undef
177              
178             B: See C for a desciption. This will override the value passed to new
179             if any.
180              
181              
182             =item accept_tos()
183              
184             In order to use the Let's Encrypt service, the account needs to accept
185             the Terms of Service. This is provided in a link header in response
186             to the new-reg ( or reg ) resource call. If the TOS have already been
187             accepted as indicated by the reg structure returned by the API this
188             call will be a noop.
189              
190             =item authz( $domain )
191              
192             C needs to be called for each domain ( called identifiers in
193             ACME speak ) in the certificate. This included the domain in the subject
194             as well as the Subject Alternate Name (SAN) fields. Each call to
195             C will result in a challenge being issued from Let's Encrypt.
196             These challenges need to be handled individually.
197              
198             =item handle_challenge( $challenge_object )
199              
200             C is called for each challenge issued by C.
201             The challenge object must be a subclass of C
202             which implements a 'handle' method. This objects handle method
203             will be passed three arguments and is expected to fulfill the
204             preconditions for the chosen challenge. The three areguments
205             are:
206              
207             fingerprint: the sha256 hex digest of the account key
208             token: the challenge token
209             url: the url returned by the challenge
210              
211             Fully describing how to handle every challenge type of out of the
212             scope of this documentation ( at least for now ). Two challenge
213             classes have been included for reference:
214              
215             C is initialized with the
216             ssh host name and the www root for the web server for the http-01
217             challenge. It will ssh to the host and create the file in
218             the correct location for challenge fulfillment.
219              
220             C is initialized with just the
221             www root for the web server for the http-01 challenge. It will
222             simply create the challenge file in the correct place on the local
223             filesystem.
224              
225             C is intended to be run in an
226             interactive manner and will stop and prompt the user with the relevant
227             information so they can fulfill the challenge manually.
228              
229             but below is an example for handling the simpleHTTP ( http-01 )
230             challenge.
231              
232              
233             =item check_challenge()
234              
235             Called after C. This will poll the challenge status
236             resource and will return when the state changes from 'pending'.
237              
238             =item cleanup_challenge()
239              
240             Called after C to remove the challenge files.
241              
242             =item $cert = sign( $csr_filename )
243              
244             =item $cert = sign( \$buffer )
245              
246             =item $cert = sign( \%explicit_args )
247              
248              
249             Call C after the challenge for each domain ( itentifier ) has
250             been fulfilled. There are three ways to call this:
251              
252             If the arg is a B it is assumed to be the filename of the
253             CSR. C will throw an error if there are problems reading the file.
254              
255             If the arg is a B reference it is assumed to be a buffer that
256             contains the CSR.
257              
258             If the arg is a B reference it contains named arguments. The arguments
259             are:
260              
261             KEY DEFAUL DESC
262             ----------- ----------- -------------------
263             filename undef The CSR Filename
264             buffer undef Buffer containing the CSR
265             format undef Explicitly state the format ( DER | PEM )
266              
267             If both C and C are set the C argument will be ignored.
268              
269             If the format is not explcitly set Protocol::ACME will look at the CSR and
270             try and determine what the format it.
271              
272             On success C will return the DER encoded signed certificate.
273              
274             =item $cert_chain = chain()
275              
276             After C has been called and a cert successfully created, C will
277             fetch and return the DER encoded certificate issuer.
278              
279             =item revoke( $certfile )
280              
281             Call C to revoke an already issued certificate. C<$certfile>
282             must point the a DER encoded form of the certificate.
283              
284             =item recovery_key()
285              
286             LE does not yet support recovery keys. This method will die when
287             called.
288              
289              
290             =back
291              
292             =cut
293              
294             package Protocol::ACME;
295              
296 6     6   24 use strict;
  6         6  
  6         86  
297 6     6   14 use warnings;
  6         5  
  6         112  
298              
299 6     6   1875 use Protocol::ACME::Exception;
  6         35  
  6         113  
300 6     6   1908 use Protocol::ACME::Utils;
  6         10  
  6         113  
301              
302 6     6   1877 use Crypt::Format;
  6         1791  
  6         115  
303 6     6   1779 use Crypt::RSA::Parse ();
  6         162178  
  6         127  
304              
305 6     6   2298 use MIME::Base64 qw( encode_base64url decode_base64url decode_base64 encode_base64 );
  6         2842  
  6         349  
306              
307 6     6   3406 use HTTP::Tiny;
  6         157108  
  6         190  
308 6     6   3381 use JSON;
  6         47255  
  6         20  
309 6     6   3193 use Digest::SHA qw( sha256 );
  6         13406  
  6         339  
310 6     6   29 use Carp;
  6         10  
  6         15231  
311              
312              
313             my $USERAGENT = "Protocol::ACME v$VERSION";
314             my $NONCE_HEADER = "replay-nonce";
315              
316             sub new
317             {
318 6     6 1 160949 my $class = shift;
319 6         11 my $self = {};
320 6         8 bless $self, $class;
321 6         18 $self->_init( @_ );
322 6         46 return $self;
323             }
324              
325             sub _init
326             {
327 6     6   8 my $self = shift;
328              
329 6         7 my $args;
330              
331 6 50       18 if ( ref $_[0] eq "HASH" )
332             {
333 0         0 $args = $_[0];
334             }
335             else
336             {
337 6         25 %$args = @_;
338             }
339              
340             # TODO: There are more elegant and well baked ways to take care of the
341             # parameter handling that I am doing here
342 6 50       39 $self->{host} = $args->{host} if exists $args->{host};
343 6 50       17 $self->{ua} = $args->{ua} if exists $args->{ua};
344 6 100       27 $self->{openssl} = $args->{openssl} if exists $args->{openssl};
345 6 100       17 $self->{debug} = $args->{debug} if exists $args->{debug};
346 6 100       15 $self->{loglevel} = exists $args->{loglevel} ? $args->{loglevel} : "error";
347 6 50       13 $self->{contact}->{mailto} = $args->{mailto} if exists $args->{mailto};
348              
349 6 100       53 if ( $self->{debug} )
350             {
351 2         4 $self->{loglevel} = "debug";
352             }
353              
354 6 50       15 if ( ! exists $self->{ua} )
355             {
356 6         50 $self->{ua} = HTTP::Tiny->new( agent => $USERAGENT, verify_SSL => 1 );
357             }
358              
359 6 50       374 if ( ! exists $self->{host} )
360             {
361 0         0 _throw( detail => "host parameter is required for Protocol::ACME::new" );
362             }
363              
364             $self->{log} = $args->{'logger'} || do
365 6   33     18 {
366             require Log::Any::Adapter;
367             Log::Any::Adapter->set('+Protocol::ACME::Logger', log_level => $self->{loglevel});
368             Log::Any->get_logger;
369             };
370              
371 6 100       6918 if ( exists $args->{account_key} )
372             {
373 4         16 $self->account_key( $args->{account_key} );
374             }
375              
376 6         35 $self->{links}->{directory} = "https://" . $self->{host} . '/directory';
377              
378 6         23 $self->{nonce} = undef;
379              
380              
381             }
382              
383             sub _throw
384             {
385 2     2   4 my (@args) = @_;
386 2 50       7 if ( scalar(@_) == 1 )
387             {
388 2         6 @args = ( detail => $_[0] );
389             }
390 2         16 croak ( Protocol::ACME::Exception->new( { @args } ) );
391             }
392              
393             sub load_key
394             {
395 0     0 0 0 my ($self, $keystring) = @_;
396 0         0 return $self->account_key( \$keystring );
397             }
398              
399             sub load_key_from_disk
400             {
401 0     0 1 0 my $self = shift;
402 0         0 my $path = shift;
403              
404 0         0 return $self->account_key($path);
405             }
406              
407             sub account_key
408             {
409 28     28 1 3093138 my $self = shift;
410 28         44 my $key = shift;
411              
412 28         114 my %args = ( filename => undef,
413             buffer => undef,
414             format => undef );
415              
416 28 100       121 if ( ! ref $key )
    100          
417             {
418 4         10 $args{filename} = $key;
419              
420 4 50       141 if ( ! -f $key )
421             {
422 0         0 _throw( "account_key file $key does not exist" );
423             }
424             }
425             elsif( ref $key eq "SCALAR" )
426             {
427 8         20 $args{buffer} = $$key;
428             }
429             else
430             {
431 16         56 @args{ keys %$key } = values %$key;
432             }
433              
434 28 100       85 if ( $args{filename} )
435             {
436 8         31 $args{buffer} = _slurp( $args{filename} );
437 8 50       27 if ( ! $args{buffer} )
438             {
439 0         0 _throw( "Could not load the account key from file $args{filename}: $!" );
440             }
441             }
442              
443 28 50       62 if ( ! $args{buffer} )
444             {
445 0         0 _throw( "Either an account key buffer or filename must be passed into account_key" );
446             }
447              
448 28 100       60 if ( ! $args{format} )
449             {
450 20 100       80 $args{format} = Protocol::ACME::Utils::looks_like_pem( $args{buffer} ) ? "PEM" : "DER";
451             }
452              
453 28         44 my $keystring = $args{buffer};
454             # TODO: This should detect/handle PKCS8-formatted private keys as well.
455 28 100       67 if ( $args{format} eq "DER" )
456             {
457 12         49 $keystring = Crypt::Format::der2pem( $keystring, "RSA PRIVATE KEY" );
458             }
459              
460 28 100       514 if ( exists $self->{openssl} )
461             {
462 15         1752 require Protocol::ACME::Key;
463             $key = Protocol::ACME::Key->new( keystring => $keystring,
464 15         127 openssl => $self->{openssl} );
465             }
466             else
467             {
468             eval
469 13         21 {
470 13         108 require Crypt::OpenSSL::RSA;
471 13         51 require Crypt::OpenSSL::Bignum;
472             };
473 13 50       33 if ( $@ )
474             {
475 0         0 die "Invoked usage requires Crypt::OpenSSL::RSA and Crypt::OpenSSL::Bignum. " .
476             "To avoid these dependencies use the openssl parameter when creating the " .
477             "Protocol::ACME object. This will use a native openssl binary instead.";
478             }
479              
480             eval
481 13         15 {
482 13         968 $key = Crypt::OpenSSL::RSA->new_private_key($keystring);
483             };
484 13 100       35 if ( $@ )
485             {
486 2         8 _throw( "Error creating a key structure from the account key" );
487             }
488             }
489              
490 25 50       109 if ( ! $key )
491             {
492 0         0 _throw( "Could not load account key into key structure" );
493             }
494              
495 25         78 $key->use_sha256_hash();
496              
497 25         63 $self->{key}->{key} = $key;
498              
499 25         542 my ( $n_b64, $e_b64 ) = map { encode_base64url(_bigint_to_binary($_)) } $key->get_key_parameters();
  116         1751  
500 25         242 $self->{key}->{n} = $n_b64;
501 25         48 $self->{key}->{e} = $e_b64;
502              
503 25         152 $self->{log}->debug( "Private key loaded" );
504              
505             }
506              
507              
508              
509              
510             sub directory
511             {
512 4     4 1 2185 my $self = shift;
513              
514 4         25 my $resp = $self->_request_get( $self->{links}->{directory} );
515              
516              
517              
518 4 50       17 if ( $resp->{status} != 200 )
519             {
520 0         0 _throw( detail => "Failed to fetch the directory for $self->{host}", resp => $resp );
521             }
522              
523 4         9 my $data = _decode_json( $resp->{content} );
524              
525 4         24 @{$self->{links}}{keys %$data} = values %$data;
  4         16  
526              
527              
528 4         41 $self->{log}->debug( "Let's Encrypt Directories loaded." );
529             }
530              
531             #
532             # Register the account or load the reg url for an existing account ( new-reg or reg )
533             #
534             sub register
535             {
536 4     4 1 1394 my $self = shift;
537 4         8 my %args = @_;
538              
539 4         8 my $obj = {};
540 4         8 $obj->{resource} = 'new-reg';
541              
542 4 50       29 if ( exists $args{mailto} )
    50          
543             {
544 0         0 push @{$obj->{contact}}, "mailto:$args{mailto}";
  0         0  
545             }
546             elsif ( exists $self->{contact}->{mailto} )
547             {
548 0         0 push @{$obj->{contact}}, "mailto:$self->{contact}->{mailto}";
  0         0  
549             }
550              
551 4         41 my $msg = _encode_json( $obj );
552              
553 4         42 my $json = $self->_create_jws( $msg );
554              
555 3         30 $self->{log}->debug( "Sending registration message" );
556              
557 3         22 my $resp = $self->_request_post( $self->{links}->{'new-reg'}, $json );
558              
559 3 50       30 if ( $resp->{status} == 409 )
    0          
560             {
561 3         21 $self->{links}->{'reg'} = $resp->{headers}->{'location'};
562              
563 3         30 $self->{log}->debug( "Known key used" );
564 3         15 $self->{log}->debug( "Refetching with location URL" );
565              
566 3         21 my $json = $self->_create_jws( _encode_json( { "resource" => 'reg' } ) );
567              
568 2         26 $resp = $self->_request_post( $self->{links}->{'reg'}, $json );
569              
570 2 50       32 if ( $resp->{status} == 202 )
571             {
572 2         13 my $links = _link_to_hash( $resp->{headers}->{'link'} );
573              
574 2         10 @{$self->{links}}{keys %$links} = values %$links;
  2         17  
575             }
576             else
577             {
578 0         0 _throw( %{ $self->{content} } );
  0         0  
579             }
580             }
581             elsif ( $resp->{status} == 201 )
582             {
583 0         0 my $links = _link_to_hash( $resp->{headers}->{'link'} );
584              
585 0         0 @{$self->{links}}{keys %$links} = values %$links;
  0         0  
586              
587 0         0 $self->{links}->{'reg'} = $resp->{headers}->{'location'};
588 0         0 $self->{log}->debug( "New key used" );
589             }
590             else
591             {
592 0         0 _throw( %{ $self->{content} } );
  0         0  
593             }
594              
595              
596 2         37 $self->{reg} = $self->{content};
597             }
598              
599             sub recovery_key
600             {
601             # LE does not yet support the key recovery resource
602             # the below can be considered debug code
603              
604 0     0 1 0 die "Let's Encrypt does not yet support key recovery";
605              
606 0         0 my $self = shift;
607              
608 0         0 my $keyfile = shift;
609              
610              
611 0         0 my $pem = _slurp( $keyfile );
612 0 0       0 _throw( "recovery_key: $keyfile: $!" ) if ! $pem;
613              
614 0         0 my $url = "https://acme-staging.api.letsencrypt.org/acme/reg/101834";
615              
616 0         0 my $der = Crypt::Format::pem2der( $pem );
617              
618 0         0 my $pub = Crypt::PK::ECC->new( \$der );
619              
620 0         0 my $public_json_text = $pub->export_key_jwk('public');
621              
622 0         0 my $hash = $pub->export_key_jwk( 'public', 1 );
623              
624             my $msg = { "resource" => "reg",
625             "recoveryToken" => {
626             "client" => { "kty" => "EC",
627             "crv" => "P-256",
628             "x" => $hash->{x},
629             "y" => $hash->{y}
630             }
631             }
632 0         0 };
633              
634 0         0 my $json = $self->_create_jws( _encode_json($msg) );
635              
636 0         0 my $resp = $self->_request_post( $url, $json );
637              
638             # TODO: This is not complete
639             }
640              
641             sub accept_tos
642             {
643 2     2 1 1112 my $self = shift;
644              
645 2 50       11 if ( exists $self->{reg}->{agreement} )
646             {
647 2         20 $self->{log}->debug( "TOS already accepted. Skipping" );
648 2         10 return;
649             }
650              
651 0         0 $self->{log}->debug( "Accepting TOS" );
652             # TODO: check for existance of terms-of-service link
653             # TODO: assert on reg url being present
654              
655             my $msg = _encode_json( { "resource" => "reg",
656             "agreement" => $self->{links}->{'terms-of-service'},
657             "key" => { "e" => $self->{key}->{e},
658             "kty" => "RSA",
659 0         0 "n" => $self->{key}->{n} } } );
660              
661              
662 0         0 my $json = $self->_create_jws( $msg );
663              
664 0         0 my $resp = $self->_request_post( $self->{links}->{'reg'}, $json );
665              
666 0 0       0 if ( $resp->{status} == 202 )
667             {
668 0         0 $self->{log}->debug( "Accepted TOS" );
669             }
670             else
671             {
672 0         0 _throw( %{ $self->{content} } );
  0         0  
673             }
674             }
675              
676             sub revoke
677             {
678 0     0 1 0 my $self = shift;
679 0         0 my $certfile = shift;
680              
681 0         0 $self->{log}->debug( "Revoking Cert" );
682              
683 0         0 my $cert = _slurp( $certfile );
684              
685 0 0       0 if ( ! $cert )
686             {
687 0         0 _throw("revoke: Could not load cert from $certfile: $!");
688             }
689              
690              
691 0         0 my $msg = _encode_json( { "resource" => "revoke-cert",
692             "certificate" => encode_base64url( $cert ) } );
693              
694              
695 0         0 my $json = $self->_create_jws( $msg );
696              
697 0         0 my $resp = $self->_request_post( $self->{links}->{'revoke-cert'}, $json );
698              
699 0 0       0 if ( $resp->{status} != 200 )
700             {
701 0         0 _throw( %{ $self->{content} } );
  0         0  
702             }
703              
704             }
705              
706             sub authz
707             {
708 0     0 1 0 my $self = shift;
709 0         0 my $domain = shift;
710              
711 0         0 $self->{log}->debug( "Sending authz message for $domain" );
712             # TODO: check for 'next' URL and that is it authz
713              
714 0         0 my $msg = _encode_json( { "identifier" => { "type" => "dns", "value" => $domain },
715             "resource" => "new-authz" } );
716              
717 0         0 my $json = $self->_create_jws( $msg );
718              
719 0         0 my $resp = $self->_request_post( $self->{links}->{next}, $json );
720              
721 0 0       0 if ( $resp->{status} == 201 )
722             {
723 0         0 $self->{challenges} = $self->{content}->{challenges};
724             }
725             else
726             {
727 0         0 _throw( %{ $self->{content} } );
  0         0  
728             }
729             }
730              
731             sub handle_challenge
732             {
733 0     0 1 0 my $self = shift;
734 0         0 my $challenge = shift;
735 0         0 my @args = @_;
736              
737 0         0 my $key = $self->{key};
738              
739 0         0 my $jwk = _encode_json( { "e" => $key->{e}, "kty" => "RSA", "n" => $key->{n} } );
740 0         0 my $token;
741             my $challenge_url;
742              
743             # TODO: this is feeling hardcoded and messy - and fragile
744             # how do we handle other auth challenges?
745             # This is hardcoded for http-01
746 0         0 for ( @{$self->{challenges}} )
  0         0  
747             {
748 0 0       0 if ( $_->{type} eq "http-01" )
749             {
750 0         0 $token = $_->{token};
751 0         0 $challenge_url = $_->{uri};
752             }
753             }
754              
755              
756 0         0 my $fingerprint = encode_base64url( sha256( $jwk ) );
757              
758 0         0 $self->{log}->debug( "Handing challenge for token: $token.$fingerprint" );
759              
760 0         0 my $ret = $challenge->handle( $token, $fingerprint, @args );
761              
762 0 0       0 if ( $ret == 0 )
763             {
764 0         0 $self->{fingerprint} = $fingerprint;
765 0         0 $self->{token} = $token;
766 0         0 $self->{links}->{challenge} = $challenge_url;
767             }
768             else
769             {
770 0         0 _throw( status => 0, detail => "Error in handling challenge: $ret", type => "challenge_exec" );
771             }
772             }
773              
774              
775             sub check_challenge
776             {
777 0     0 1 0 my $self = shift;
778              
779 0         0 my $msg = _encode_json( { "resource" => "challenge", "keyAuthorization" => $self->{token} . '.' . $self->{fingerprint} } );
780              
781 0         0 my $json = $self->_create_jws( $msg );
782              
783              
784 0         0 my $resp = $self->_request_post( $self->{links}->{challenge}, $json );
785              
786 0         0 my $status_url = $self->{content}->{uri};
787              
788             # TODO: check for failure of challenge check
789             # TODO: check for other HTTP failures
790              
791 0         0 $self->{log}->debug( "Polling for challenge fulfillment" );
792 0         0 while( 1 )
793             {
794 0         0 $self->{log}->debug( "Status: $self->{content}->{status}" );
795 0 0       0 if ( $self->{content}->{status} eq "pending" )
    0          
796             {
797 0         0 sleep(2);
798 0         0 $resp = $self->_request_get( $status_url );
799             }
800             elsif ( $self->{content}{status} eq "invalid" )
801             {
802 0         0 _throw(%{ $self->{content} });
  0         0  
803             }
804             else
805             {
806 0         0 last;
807             }
808             }
809             }
810              
811             sub cleanup_challenge
812             {
813 0     0 1 0 my $self = shift;
814 0         0 my $challenge = shift;
815 0         0 return $challenge->cleanup();
816             }
817              
818             sub sign
819             {
820 0     0 1 0 my $self = shift;
821 0         0 my $csr = shift;
822              
823 0         0 $self->{log}->debug( "Signing" );
824              
825 0         0 my %args = ( filename => undef,
826             buffer => undef,
827             format => undef );
828              
829 0 0       0 if ( ! ref $csr )
    0          
830             {
831 0         0 $args{filename} = $csr;
832             }
833             elsif( ref $csr eq "SCALAR" )
834             {
835 0         0 $args{buffer} = $$csr;
836             }
837             else
838             {
839 0         0 @args{keys %$csr} = values %$csr;
840             }
841              
842 0 0       0 if ( $args{filename} )
843             {
844 0         0 $args{buffer} = _slurp( $args{filename} );
845 0 0       0 if ( ! $args{buffer} )
846             {
847 0         0 _throw( "Could not load CSR from file $args{filename}" );
848             }
849             }
850              
851 0 0       0 if ( ! $args{buffer} )
852             {
853 0         0 _throw( "Either a buffer or filename must be passed to sign" );
854             }
855              
856 0 0       0 if ( ! $args{format} )
857             {
858 0 0       0 $args{format} = Protocol::ACME::Utils::looks_like_pem( $args{buffer} ) ? "PEM" : "DER";
859             }
860              
861 0 0       0 my $der = $args{format} eq "DER" ? $args{buffer} : Crypt::Format::pem2der( $args{buffer} );
862              
863 0         0 my $msg = _encode_json( { "resource" => "new-cert", "csr" => encode_base64url( $der ) } );
864              
865 0         0 my $json = $self->_create_jws( $msg );
866              
867 0         0 my $resp = $self->_request_post( $self->{links}->{'new-cert'}, $json, 1 );
868              
869 0 0       0 if ( $resp->{status} != 201 )
870             {
871 0         0 _throw( %{_decode_json($resp->{content}) } );
  0         0  
872             }
873              
874 0         0 my $links = _link_to_hash( $resp->{headers}->{'link'} );
875              
876 0 0       0 $self->{links}->{chain} = $links->{up} if exists $links->{up};
877 0 0       0 $self->{links}->{cert} = $resp->{headers}->{location} if exists $resp->{headers}->{location};
878              
879 0         0 $self->{cert} = $resp->{content};
880              
881 0         0 return $self->{cert};
882             }
883              
884             sub chain
885             {
886 0     0 1 0 my $self = shift;
887              
888 0 0       0 if ( ! exists $self->{links}->{chain} )
889             {
890 0         0 _throw( "URL for the cert chain missing. Has sign() been called yet?" );
891             }
892              
893 0         0 my $resp = $self->_request_get( $self->{links}->{chain}, 1 );
894              
895 0 0       0 if ( $resp->{status} != 200 )
896             {
897             _throw( detail => "Error received fetching the certificate chain",
898 0         0 status => $resp->{status} );
899             }
900              
901 0         0 $self->{chain} = $resp->{content};
902              
903 0         0 return $self->{chain};
904             }
905              
906             #############################################################
907             ### "Private" functions
908              
909             sub _request_get
910             {
911 4     4   4 my $self = shift;
912 4         8 my $url = shift;
913 4   50     35 my $nodecode = shift || 0;
914              
915 4         122 my $resp = $self->{ua}->get( $url );
916              
917 4         1772980 $self->{nonce} = $resp->{headers}->{$NONCE_HEADER};
918 4         109 $self->{json} = $resp->{content};
919              
920             #Exception here should be fatal.
921 4         8 $self->{content} = undef;
922 4 50       35 $self->{content} = _decode_json( $resp->{content} ) unless $nodecode;
923              
924 4         11 $self->{response} = $resp;
925              
926 4         12 return $resp;
927             }
928              
929             sub _request_post
930             {
931 5     5   10 my $self = shift;
932 5         9 my $url = shift;
933 5         6 my $content = shift;
934 5   50     39 my $nodecode = shift || 0;
935              
936 5         246 my $resp = $self->{ua}->post( $url, { content => $content } );
937              
938 5         1191759 $self->{nonce} = $resp->{headers}->{$NONCE_HEADER};
939              
940 5         17 $self->{json} = $resp->{content};
941              
942             #Let exception from decode_json() propagate:
943             #if we failed to decode the JSON, that’s a show-stopper.
944 5         13 $self->{content} = undef;
945 5 50       67 $self->{content} = _decode_json( $resp->{content} ) unless $nodecode;
946              
947 5         14 $self->{response} = $resp;
948              
949 5         44 return $resp;
950             }
951              
952             sub _create_jws
953             {
954 7     7   10 my $self = shift;
955              
956 7         12 my $msg = shift;
957 7         30 return _create_jws_internal( $self->{key}, $msg, $self->{nonce} );
958             }
959              
960              
961             #############################################################
962             ### Helper functions - not class methods
963              
964             sub _slurp
965             {
966 8     8   10 my $filename = shift;
967              
968 8 50       445 open my $fh, '<', $filename or return undef;
969              
970 8 50       115 sysread( $fh, my $content, -s $fh ) or return undef;
971              
972 8         261 return $content;
973             }
974              
975              
976             sub _link_to_hash
977             {
978 2     2   5 my $arrayref = shift;
979 2         5 my $links;
980              
981 2 50       13 return {} unless $arrayref;
982              
983 2 50       8 if ( ! ref $arrayref )
984             {
985 0         0 $arrayref = [ $arrayref ];
986             }
987              
988 2         7 for my $link ( @$arrayref )
989             {
990 4         17 my ( $value, $key ) = split( ';', $link );
991 4         21 my ($url) = $value =~ /<([^>]*)>/;
992 4         15 my ($rel) = $key =~ /rel=\"([^"]*)"/;
993              
994 4 50 33     24 if ( $url && $rel )
995             {
996 4         13 $links->{$rel} = $url;
997             }
998             else
999             {
1000             # TODO: Something wonderful
1001             }
1002             }
1003              
1004 2         7 return $links;
1005             }
1006              
1007             sub _bigint_to_binary {
1008 116     116   108 my ( $bigint ) = @_;
1009              
1010             # TODO: Inelegant hack to deal with different Bignum implementations
1011 116         84 my $hex;
1012 116 100       306 if ( UNIVERSAL::isa( $bigint, "Math::BigInt" ) )
1013             {
1014 28         94 $hex = substr( $bigint->as_hex(), 2 );
1015             #Prefix a 0 as needed to get an even number of digits.
1016 28 100       158271 if (length($hex) % 2) {
1017 14         35 substr( $hex, 0, 0, 0 );
1018             }
1019              
1020 28         192 return pack 'H*', $hex;
1021             }
1022             else
1023             {
1024 88         272 $bigint->to_bin();
1025             }
1026              
1027             }
1028              
1029             sub _create_jws_internal
1030             {
1031 7     7   10 my $key = shift;
1032 7         11 my $msg = shift;
1033 7         9 my $nonce = shift;
1034              
1035 7         23 my $protected_header = '{"nonce": "' . $nonce . '"}';
1036              
1037 7         40 my $sig = encode_base64url( $key->{key}->sign( encode_base64url($protected_header) . "." . encode_base64url($msg) ) );
1038              
1039             my $jws = { header => { alg => "RS256", jwk => { "e" => $key->{e}, "kty" => "RSA", "n" => $key->{n} } },
1040 5         5379 protected => encode_base64url( $protected_header ),
1041             payload => encode_base64url( $msg ),
1042             signature => $sig };
1043              
1044 5         84 my $json = _encode_json( $jws );
1045              
1046 5         53 return $json;
1047              
1048             }
1049              
1050             sub _decode_json
1051             {
1052 13     13   25 my $ref = shift;
1053              
1054 13         22 my $json = "";
1055              
1056             eval
1057 13         16 {
1058 13         356 $json = JSON->new->allow_nonref->decode($ref);
1059             };
1060              
1061 13         66 return $json;
1062             }
1063              
1064             sub _encode_json
1065             {
1066 12     12   20 my $ref = shift;
1067             # my $json = JSON->new();
1068             # $json->canonical();
1069             # return $json->encode($ref);
1070 12         359 return JSON->new->canonical->encode($ref);
1071             }
1072              
1073              
1074             =head1 AUTHOR
1075              
1076             Stephen Ludin, C<< >>
1077              
1078             =head1 BUGS
1079              
1080             Please report any bugs or feature requests to C, or through
1081             the web interface at L. I will be notified, and then you'll
1082             automatically be notified of progress on your bug as I make changes.
1083              
1084             =head1 REPOSITORY
1085              
1086             https://github.com/sludin/Protocol-ACME
1087              
1088              
1089             =head1 SUPPORT
1090              
1091             You can find documentation for this module with the perldoc command.
1092              
1093             perldoc Protocol::ACME
1094              
1095              
1096             You can also look for information at:
1097              
1098             =over 4
1099              
1100             =item * RT: CPAN's request tracker (report bugs here)
1101              
1102             L
1103              
1104             =item * AnnoCPAN: Annotated CPAN documentation
1105              
1106             L
1107              
1108             =item * CPAN Ratings
1109              
1110             L
1111              
1112             =item * Search CPAN
1113              
1114             L
1115              
1116             =back
1117              
1118              
1119             =head1 CONTRIBUTORS
1120              
1121             Felipe Gasper, C<< >>
1122              
1123             =head1 ACKNOWLEDGEMENTS
1124              
1125              
1126              
1127             =head1 LICENSE AND COPYRIGHT
1128              
1129             Copyright 2015 Stephen Ludin.
1130              
1131             This program is free software; you can redistribute it and/or modify it
1132             under the terms of the the Artistic License (2.0). You may obtain a
1133             copy of the full license at:
1134              
1135             L
1136              
1137             Any use, modification, and distribution of the Standard or Modified
1138             Version 1.01
1139             distributing the Package, you accept this license. Do not use, modify,
1140             or distribute the Package, if you do not accept this license.
1141              
1142             If your Modified Version has been derived from a Modified Version made
1143             by someone other than you, you are nevertheless required to ensure that
1144             your Modified Version complies with the requirements of this license.
1145              
1146             This license does not grant you the right to use any trademark, service
1147             mark, tradename, or logo of the Copyright Holder.
1148              
1149             This license includes the non-exclusive, worldwide, free-of-charge
1150             patent license to make, have made, use, offer to sell, sell, import and
1151             otherwise transfer the Package with respect to any patent claims
1152             licensable by the Copyright Holder that are necessarily infringed by the
1153             Package. If you institute patent litigation (including a cross-claim or
1154             counterclaim) against any party alleging that the Package constitutes
1155             direct or contributory patent infringement, then this Artistic License
1156             to you shall terminate on the date that such litigation is filed.
1157              
1158             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
1159             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
1160             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
1161             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
1162             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
1163             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
1164             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
1165             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1166              
1167              
1168             =cut
1169              
1170             1; # End of Protocol::ACME