]>
Raphaël G. Git Repositories - acme/blob - Acme.pm 
adcc99e0fbab09d0b0555dd35c9e05de577c0859
   1  # This file is part of Acmepl    3  # Acmepl is is free software: you can redistribute it and/or modify    4  # it under the terms of the GNU General Public License as published by    5  # the Free Software Foundation, either version 3 of the License, or    6  # (at your option) any later version.    8  # This program is distributed in the hope that it will be useful,    9  # but WITHOUT ANY WARRANTY; without even the implied warranty of   10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the   11  # GNU General Public License for more details.   13  # You should have received a copy of the GNU General Public License   14  # along with this program.  If not, see <http://www.gnu.org/licenses/>.   16  # Copyright (C) 2016 - 2017 Raphaël Gertz <acme@rapsys.eu>   26  use  filetest 
qw(access) ;   30  our  @ISA  =  qw(Exporter) ;   31  our  @EXPORT_OK  =  qw(VERSION) ;   34  use  Carp 
qw(carp confess) ;   35  use  Date
:: Parse 
qw(str2time) ;   36  use  Digest
:: SHA 
qw(sha256_base64) ;   38  use  File
:: Copy 
qw(copy) ;   39  use  File
:: Path 
qw(make_path) ;   40  use  File
:: Slurp 
qw(read_file write_file) ;   41  use  File
:: Temp
;  # qw( :seekable );   42  use  IPC
:: System
:: Simple 
qw(capturex) ;   43  use  JSON 
qw(from_json to_json) ;   45  use  MIME
:: Base64 
qw(encode_base64url encode_base64) ;   47  use  POSIX 
qw(EXIT_FAILURE) ;   54  #XXX: see https://letsencrypt.github.io/acme-spec/ (probably based on https://ietf-wg-acme.github.io/acme/)   55  #XXX: see jwk rfc http://www.rfc-editor.org/rfc/rfc7517.txt   56  #XXX: see javascript implementation https://github.com/diafygi/gethttpsforfree/blob/gh-pages/js/index.js   60          # Request certificate file name   61          REQUEST_CSR 
=>  'request.der' ,   70          ACME_CERT 
=>  'https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem' ,   71          ACME_DIR 
=>  'https://acme-staging.api.letsencrypt.org/directory' ,   72          ACME_PROD_DIR 
=>  'https://acme-v01.api.letsencrypt.org/directory' ,   85  #XXX: tie to Tie::IxHash to keep a stable ordering of hash keys   94  #                       kty => uc(KEY_TYPE),  101  tie
( our  %jwk ,  'Tie::IxHash' ,  pubkey 
=>  undef ,  jwk 
=>  undef ,  thumbprint 
=>  undef );  102  tie
(%{ $jwk { jwk
}},  'Tie::IxHash' ,  alg 
=>  'RS256' ,  jwk 
=>  undef );  103  #XXX: strict ordering only really needed here for thumbprint sha256 digest  104  tie
(%{ $jwk { jwk
}{ jwk
}},  'Tie::IxHash' ,  e 
=>  undef ,  kty 
=>  uc ( KEY_TYPE
),  n 
=>  undef );  109          my  ( $class ,  $debug ,  $domain ,  $config ) =  @_ ;  114          # Link self to package  115          bless ( $self ,  $class );  118          $self ->{ debug
} =  $debug ;  121          $self ->{ domain
} =  $domain ;  124          $self ->{ config
} =  $config ;  127          @{ $self ->{ domains
}} = ( $domain ->{ domain
}, @{ $domain ->{ domains
}});  129          # Add extra check to mail validity  130          #XXX: mxcheck fail if there is only a A record on the domain  131          my  $ev  =  Email
:: Valid-
> new (- fqdn 
=>  1 , - tldcheck 
=>  1 , - mxcheck 
=>  1 );  133          # Show error if check fail  134          if  (!  defined  $ev -> address ( $self ->{ domain
}{ mail
})) {  135                  map  {  carp 
'failed check: ' . $_  if  ( $self ->{ debug
}) }  $ev -> details ();  136                  confess 
'Email::Valid->address failed' ;  140          $self ->{ mail
} =  $self ->{ domain
}{ mail
};  143          my  $res  =  new Net
:: DNS
:: Resolver
();  150                  unless  (( $tld ) =  $_  =~  m/\.(\w+)$/ ) {  151                          confess 
$_ . ' \' s tld extraction failed' ;  154                  # Check if tld exists  155                  unless ( Net
:: Domain
:: TLD
:: tld_exists
( $tld )) {  156                          confess 
$tld . ' tld from ' . $_ . ' don \' t exists' ;  159                  # Check if we get dns answer  160                  #XXX: only search A type because letsencrypt don't support ipv6 (AAAA) yet  161                  unless ( my  $rep  =  $res -> search ( $_ ,  'A' )) {  162                          confess 
'search A record for ' . $_ . ' failed' ;  164                          unless  ( scalar map  {  $_ -> type  eq  'A'  ?  1  : (); }  $rep -> answer ) {  165                                  confess 
'search recursively A record for ' . $_ . ' failed' ;  168          } @{ $self ->{ domains
}};  170          # Return class reference  174  # Prepare environement  178          # Extract cert directory and filename  179          my  ( $certFile ,  $certDir ) =  File
:: Spec-
> splitpath ( $self ->{ domain
}{ cert
});  181          # Extract key directory and filename  182          my  ( $keyFile ,  $keyDir ) =  File
:: Spec-
> splitpath ( $self ->{ domain
}{ key
});  184          # Extract account directory and filename  185          my  ( $accountFile ,  $accountDir ) =  File
:: Spec-
> splitpath ( $self ->{ domain
}{ account
});  189                  make_path
( $certDir ,  $keyDir ,  $accountDir ,  $self ->{ config
}{ pending
}. '/' . $self ->{ mail
}. '.' .( $self ->{ domain
}{ prod
} ?  'prod'  :  'staging' ), { error 
=>  \
my  $err });  192                                  my  ( $file ,  $msg ) =  %$_ ;  193                                  carp 
( $file  eq  ''  ?  ''  :  $file . ': ' ). $msg  if  ( $self ->{ debug
});  195                          confess 
'make_path failed' ;  200          $ua  =  LWP
:: UserAgent-
> new ;  201          $ua -> agent ( __PACKAGE__
. '/' . VERSION
);  203          # Check that certificate is writable  204          unless  (- w 
$certDir  || - w 
$self ->{ domain
}{ cert
}) {  205                  confess
( 'Directory ' . $certDir . ' or file ' . $self ->{ domain
}{ cert
}. ' must be writable: ' . $! );  208          # Check that key is writable  209          unless  (- r 
$self ->{ domain
}{ key
} || - w 
$keyDir ) {  210                  confess
( 'File ' . $self ->{ domain
}{ key
}. ' must be readable or directory ' . $keyDir . ' must be writable: ' . $! );  213          # Check that account is writable  214          unless  (- r 
$self ->{ domain
}{ account
} || - w 
$accountDir ) {  215                  confess
( 'File ' . $self ->{ domain
}{ account
}. ' must be readable or directory ' . $accountDir . ' must be writable: ' . $! );  218          # Backup old certificate if possible  219          if  (- w 
$certDir  && - f 
$self ->{ domain
}{ cert
}) {  220                  my  ( $dt ,  $suffix ) =  undef ;  222                  # Extract datetime suffix  223                  $suffix  = ( $dt  =  DateTime-
> from_epoch ( epoch 
=>  stat ( $self ->{ domain
}{ cert
})-> mtime ))-> ymd ( '' ). $dt -> hms ( '' );  225                  # Rename old certificate  226                  unless ( copy
( $self ->{ domain
}{ cert
},  $self ->{ domain
}{ cert
}. '.' . $suffix )) {  227                          carp
( 'Copy ' . $self ->{ domain
}{ cert
}. ' to ' . $self ->{ domain
}{ cert
}. '.' . $suffix . ' failed: ' . $! );  235          open ( $_stderr ,  '>&STDERR' )  or die  $! ;  237          close ( STDERR
)  or die  $! ;  239          open ( STDERR
,  '>' ,  '/dev/null' )  or die  $! ;  247          open ( STDERR
,  '>&' ,  $_stderr )  or die  $! ;  250  # Generate required keys  254          # Generate account and server key if required  256                  # Check key existence  261                          #XXX: we drop stderr here because openssl can't be quiet on this command  262                          capturex
( 'openssl' , ( 'genrsa' ,  '-out' ,  $_ ,  KEY_SIZE
));  266          } ( $self ->{ domain
}{ account
},  $self ->{ domain
}{ key
});  268          # Extract modulus and publicExponent jwk  269          #XXX: same here we tie to keep ordering  270          tie
(%{ $self ->{ account
}},  'Tie::IxHash' ,  %jwk );  272                  if  ( /^Modulus=([0-9A-F]+)$/ ) {  273                          # Extract to binary from hex and convert to base64 url  274                          $self ->{ account
}{ jwk
}{ jwk
}{ n
} =  encode_base64url
( pack ( "H*" ,  $1 ) =~  s/^\0+//r );  275                  }  elsif  ( /^publicExponent:\s([0-9]+)\s\(0x[0-1]+\)$/ ) {  276                          # Extract to binary from int, trim leading zeros and convert to base64 url  277                          chomp  ( $self ->{ account
}{ jwk
}{ jwk
}{ e
} =  encode_base64url
( pack ( "N" ,  $1 ) =~  s/^\0+//r ));  279          }  capturex
( 'openssl' , ( 'rsa' ,  '-text' ,  '-in' ,  $self ->{ domain
}{ account
},  '-noout' ,  '-modulus' ));  283          # Extract account public key  284          $self ->{ account
}{ pubkey
} =  join ( '' ,  map  {  chomp ;  $_ ; }  capturex
( 'openssl' , ( 'rsa' ,  '-in' ,  $self ->{ domain
}{ account
},  '-pubout' )));  289          #XXX: convert base64 to base64 url  290          $self ->{ account
}{ thumbprint
} = ( sha256_base64
( to_json
( $self ->{ account
}{ jwk
}{ jwk
})) =~  s/=+\z//r ) =~  tr
[+/][- _
] r
;  293  # Generate certificate request  297          # Openssl config template  298          my  $oct  =  File
:: Temp-
> new ();  300          # Save data start position  303          # Load template from data  304          map  {  s/__EMAIL_ADDRESS__/$self->{mail}/ ;  s/__COMMON_NAME__/$self->{domains}[0]/ ;  print  $oct $_ ; } < DATA
>;  309          # Append domain names  311          map  {  print  $oct  'DNS.' . $i++ . ' = ' . $_ . " \n " ; } @{ $self ->{ domains
}};  314          capturex
( 'openssl' , ( 'req' ,  '-new' ,  '-outform' ,  'DER' ,  '-key' ,  $self ->{ domain
}{ key
},  '-config' ,  $oct -> filename ,  '-out' ,  $self ->{ config
}{ pending
}. '/' . $self ->{ mail
}. '.' .( $self ->{ domain
}{ prod
} ?  'prod'  :  'staging' ). '/' . REQUEST_CSR
));  328          my  $dir  =  $self ->{ prod
} ?  ACME_PROD_DIR 
:  ACME_DIR
;  331          my  $req  =  HTTP
:: Request-
> new ( GET 
=>  $dir . '?' . $time );  334          my  $res  =  $ua -> request ( $req );  337          unless  ( $res -> is_success ) {  338                  confess 
'GET ' . $dir . '?' . $time . ' failed: ' . $res -> status_line ;  342          $self ->{ nonce
} =  $res -> headers ->{ 'replay-nonce' };  344          # Merge uris in self content  345          %$self  = ( %$self , %{ from_json
( $res -> content )});  350          my  ( $self ,  $uri ,  $payload ) =  @_ ;  353          my  $protected  =  encode_base64url
( to_json
({ nonce 
=>  $self ->{ nonce
}}));  356          $payload  =  encode_base64url
( to_json
( $payload ));  359          my  $stf  =  File
:: Temp-
> new ();  361          # Append protect.payload to stf  362          print  $stf $protected . '.' . $payload ;  367          # Generate digest of stf  368          my  $signature  =  encode_base64url
( join ( '' ,  capturex
( 'openssl' , ( 'dgst' ,  '-sha256' ,  '-binary' ,  '-sign' ,  $self ->{ domain
}{ account
},  $stf -> filename ))) =~  s/^\0+//r );  371          my  $req  =  HTTP
:: Request-
> new ( POST 
=>  $uri );  373          # Set new-reg request content  374          $req -> content ( to_json
({  375                  header 
=>  $self ->{ account
}{ jwk
},  376                  protected 
=>  $protected ,  378                  signature 
=>  $signature  382          my  $res  =  $ua -> request ( $req );  385          if  ( defined  $res -> headers ->{ 'replay-nonce' }) {  386                  $self ->{ nonce
} =  $res -> headers ->{ 'replay-nonce' };  393  # Resolve dns and check content  394  #XXX: see https://community.centminmod.com/threads/looks-like-letsencrypt-dns-01-is-ready.5845/#12 for example  396          my  ( $self ,  $domain ,  $token ) =  @_ ;  398          # Generate signature from content  399          my  $signature  = (( sha256_base64
( $token . '.' . $self ->{ account
}{ thumbprint
})) =~  s/=+\z//r ) =~  tr
[+/][- _
] r
;  402          $domain  =  '_acme-challenge.' . $domain . '.' ;  405          my  $res  =  new Net
:: DNS
:: Resolver
();  407          # Check if we get dns answer  408          unless ( my  $rep  =  $res -> search ( $domain ,  'TXT' )) {  409                  carp 
'TXT record search for ' . $domain . ' failed'  if  ( $self ->{ debug
});  412                  unless  ( scalar map  {  $_ -> type  eq  'TXT'  &&  $_ -> txtdata  =~  /^$signature$/  ?  1  : (); }  $rep -> answer ) {  413                          carp 
'TXT record recursive search for ' . $domain . ' failed'  if  ( $self ->{ debug
});  421  # Get uri and check content  423          my  ( $self ,  $domain ,  $token ) =  @_ ;  426          my  $req  =  HTTP
:: Request-
> new ( GET 
=>  'http://' . $domain . '/.well-known/acme-challenge/' . $token );  428          # Check if thumbprint is writeable  429          if  (- w 
$self ->{ config
}{ thumbprint
}) {  430                  # Try to write thumbprint  431                  write_file
( $self ->{ config
}{ thumbprint
},  $self ->{ account
}{ thumbprint
});  435          my  $res  =  $ua -> request ( $req );  438          unless  ( $res -> is_success ) {  439                  carp 
'GET http://' . $domain . '/.well-known/acme-challenge/' . $token . ' failed: ' . $res -> status_line  if  ( $self ->{ debug
});  443          # Handle invalid content  444          unless ( $res -> content  =~  /^$token.$self->{account}{thumbprint}\s*$/ ) {  445                  carp 
'GET http://' . $domain . '/.well-known/acme-challenge/' . $token . ' content match failed: /^' . $token . '.' . $self ->{ account
}{ thumbprint
}. '\s* $/  !~ ' . $res -> content  if  ( $self ->{ debug
});  454  #XXX: see doc at https://ietf-wg-acme.github.io/acme/#rfc.section.6.3  458          # Post new-reg request  459          #XXX: contact array may contain a tel:+33612345678 for example  460          my  $res  =  $self -> _post ( $self ->{ 'new-reg' }, { resource 
=>  'new-reg' ,  contact 
=> [ 'mailto:' . $self ->{ mail
}],  agreement 
=>  $self ->{ term
}});  463          unless  ( $res -> is_success  ||  $res -> code  eq  409 ) {  464                  confess 
'POST ' . $self ->{ 'new-reg' }. ' failed: ' . $res -> status_line ;  467          # Update mail informations  468          if  ( $res -> code  eq  409 ) {  469                  # Save registration uri  470                  $self ->{ 'reg' } =  $res -> headers ->{ location
};  473                  #XXX: contact array may contain a tel:+33612345678 for example  474                  $res  =  $self -> _post ( $self ->{ 'reg' }, { resource 
=>  'reg' ,  contact 
=> [ 'mailto:' . $self ->{ mail
}]});  477                  unless  ( $res -> is_success ) {  478                          confess 
'POST ' . $self ->{ 'reg' }. ' failed: ' . $res -> status_line ;  487          # Create challenges hash  488          %{ $self ->{ challenges
}} = ();  493          # Create or load auth request for each domain  499                  my  $file  =  $self ->{ config
}{ pending
}. '/' . $self ->{ mail
}. '.' .( $self ->{ domain
}{ prod
} ?  'prod'  :  'staging' ). '/' . $_ ;  501                  # Load auth request content or post a new one  502                  #TODO: add more check on cache file ???  504                          #XXX: use eval to workaround a fatal in from_json  506                                  # Check that file exists  509                                  ( $content  =  read_file
( $file )) &&  511                                  ( $content  =  from_json
( $content ))  513                          } || ( str2time
( $content ->{ expires
}) <=  time ()+ 3600 )  515                          # Post new-authz request  516                          my  $res  =  $self -> _post ( $self ->{ 'new-authz' }, { resource 
=>  'new-authz' ,  identifier 
=> { type 
=>  'dns' ,  value 
=>  $_ },  existing 
=>  'accept' });  519                          unless  ( $res -> is_success ) {  520                                  confess 
'POST ' . $self ->{ 'new-authz' }. ' for ' . $_ . ' failed: ' . $res -> status_line ;  524                          $content  =  from_json
( $res -> content );  527                          unless  ( defined  $content ->{ identifier
}{ value
} &&  $content ->{ identifier
}{ value
}  eq  $_ ) {  528                                  confess 
'domain matching ' . $content ->{ identifier
}{ value
}. ' for ' . $_ . ' failed: ' . $res -> status_line ;  532                          unless  ( $content ->{ status
}  eq  'valid'  or  $content ->{ status
}  eq  'pending' ) {  533                                  confess 
'POST ' . $self ->{ 'new-authz' }. ' for ' . $_ . ' failed: ' . $res -> status_line ;  537                          write_file
( $file ,  to_json
( $content ));  541                  %{ $self ->{ challenges
}{ $_ }} = (  542                          status 
=>  $content ->{ status
},  543                          expires 
=>  $content ->{ expires
},  548                  if  ( $content ->{ status
}  eq  'pending' ) {  549                          # Extract validation data  550                          foreach  my  $challenge  (@{ $content ->{ challenges
}}) {  551                                  # One test already validated this auth request  552                                  if  ( $self ->{ challenges
}{ $_ }{ status
}  eq  'valid' ) {  554                                  }  elsif  ( $challenge ->{ status
}  eq  'valid' ) {  555                                          $self ->{ challenges
}{ $_ }{ status
} =  $challenge ->{ status
};  557                                  }  elsif  ( $challenge ->{ status
}  eq  'pending' ) {  560                                                  ( $challenge ->{ type
} =~  /^http-01$/  and  $self -> _httpCheck ( $_ ,  $challenge ->{ token
}))  or  561                                                  ( $challenge ->{ type
} =~  /^dns-01$/  and  $self -> _dnsCheck ( $_ ,  $challenge ->{ token
}))  563                                                  # Post challenge request  564                                                  my  $res  =  $self -> _post ( $challenge ->{ uri
}, { resource 
=>  'challenge' ,  keyAuthorization 
=>  $challenge ->{ token
}. '.' . $self ->{ account
}{ thumbprint
}});  567                                                  unless  ( $res -> is_success ) {  568                                                          confess 
'POST ' . $challenge ->{ uri
}. ' failed: ' . $res -> status_line ;  572                                                  my  $content  =  from_json
( $res -> content );  575                                                  if  ( $content ->{ status
}  eq  'valid' ) {  576                                                          $self ->{ challenges
}{ $_ }{ status
} =  $content ->{ status
};  577                                                  # Check is still polling  578                                                  }  elsif  ( $content ->{ status
}  eq  'pending' ) {  579                                                          # Add to poll list for later use  580                                                          push (@{ $self ->{ challenges
}{ $_ }{ polls
}}, {  581                                                                  type 
=> ( split ( /-/ ,  $challenge ->{ type
}))[ 0 ],  582                                                                  status 
=>  $content ->{ status
},  583                                                                  poll 
=>  $content ->{ uri
}  589                          # Check if check is challenge still in pending and no polls  590                          if  ( $self ->{ challenges
}{ $_ }{ status
}  eq  'pending'  &&  scalar  @{ $self ->{ challenges
}{ $_ }{ polls
}} ==  0 ) {  591                                  # Loop on all remaining challenges  592                                  foreach  my  $challenge  (@{ $content ->{ challenges
}}) {  593                                          # Display help for http-01 check  594                                          if  ( $challenge ->{ type
}  eq  'http-01' ) {  595                                                  print  STDERR 
'Create URI http://' . $_ . '/.well-known/acme-challenge/' . $challenge ->{ token
}. ' with content ' . $challenge ->{ token
}. '.' . $self ->{ account
}{ thumbprint
}. " \n " ;  596                                          # Display help for dns-01 check  597                                          }  elsif  ( $challenge ->{ type
}  eq  'dns-01' ) {  598                                                  print  STDERR 
'Create TXT record _acme-challenge.' . $_ . '. with value ' .((( sha256_base64
( $challenge ->{ token
}. '.' . $self ->{ account
}{ thumbprint
})) =~  s/=+\z//r ) =~  tr
[+/][- _
] r
). " \n " ;  603          } @{ $self ->{ domains
}};  609          while  (-- $remaining  >=  0  and scalar map  {  $_ ->{ status
}  eq  'valid'  ?  1  : (); }  values  %{ $self ->{ challenges
}}) {  612                  # Poll remaining pending  617                          # Poll remaining polls  620                                  my  $req  =  HTTP
:: Request-
> new ( GET 
=>  $_ ->{ poll
});  623                                  my  $res  =  $ua -> request ( $req );  626                                  unless  ( $res -> is_success ) {  627                                          carp 
'GET ' . $self ->{ challenges
}{ $_ }{ http_challenge
}. ' failed: ' . $res -> status_line  if  ( $self ->{ debug
});  631                                  my  $content  =  from_json
( $res -> content );  634                                  if  ( $content ->{ status
}  ne  'pending' ) {  635                                          $self ->{ challenges
}{ $domain }{ status
} =  $content ->{ status
};  637                          } @{ $self ->{ challenges
}{ $_ }{ polls
}};  638                  }  map  {  $self ->{ challenges
}{ $_ }{ status
}  eq  'pending'  ?  $_  : (); }  keys  %{ $self ->{ challenges
}};  641          # Check if thumbprint is writeable  642          if  (- w 
$self ->{ config
}{ thumbprint
}) {  643                  # Try to write thumbprint  644                  write_file
( $self ->{ config
}{ thumbprint
},  '' );  647          # Stop here with remaining chanllenge  648          if  ( scalar map  { !  defined  $_ ->{ status
}  or  $_ ->{ status
}  ne  'valid'  ?  1  : (); }  values  %{ $self ->{ challenges
}}) {  649                  # Deactivate all activated domains   650                  #XXX: not implemented by letsencrypt  652                  #       # Post deactivation request  653                  #       my $res = $self->_post($self->{challenges}{$_}{http_uri}, {resource => 'authz', status => 'deactivated'});  655                  #       unless ($res->is_success) {  656                  #               confess 'POST '.$self->{challenges}{$_}{http_uri}.' failed: '.$res->status_line;  658                  #} map { $self->{challenges}{$_}{status} eq 'valid' ? $_ : () } keys %{$self->{challenges}};  660                  # Stop here as a domain of csr list failed authorization  661                  if  ( $self ->{ debug
}) {  662                          my  @domains  =  map  { !  defined  $self ->{ challenges
}{ $_ }{ status
}  or  $self ->{ challenges
}{ $_ }{ status
}  ne  'valid'  ?  $_  : (); }  keys  %{ $self ->{ challenges
}};  663                          confess 
'Fix the challenge' .( scalar  @domains  >  1 ? 's' : '' ). ' for domain' .( scalar  @domains  >  1 ? 's' : '' ). ': ' . join ( ', ' ,  @domains );  675          open ( my  $fh ,  '<' ,  $self ->{ config
}{ pending
}. '/' . $self ->{ mail
}. '.' .( $self ->{ domain
}{ prod
} ?  'prod'  :  'staging' ). '/' . REQUEST_CSR
)  or die  $! ;  678          my  $csr  =  encode_base64url
( join ( '' , < $fh >) =~  s/^\0+//r );  681          close ( $fh )  or die  $! ;  683          # Post certificate request  684          my  $res  =  $self -> _post ( $self ->{ 'new-cert' }, { resource 
=>  'new-cert' ,  csr 
=>  $csr });  687          unless  ( $res -> is_success ) {  688                  confess 
'POST ' . $self ->{ 'new-cert' }. ' failed: ' . $res -> status_line ;  692          open ( $fh ,  '>' ,  $self ->{ domain
}{ cert
})  or die  $! ;  695          print  $fh  '-----BEGIN CERTIFICATE-----' . " \n " . encode_base64
( $res -> content ). '-----END CERTIFICATE-----' . " \n " ;  698          my  $req  =  HTTP
:: Request-
> new ( GET 
=>  ACME_CERT
);  701          $res  =  $ua -> request ( $req );  704          unless  ( $res -> is_success ) {  705                  carp 
'GET ' . ACME_CERT
. ' failed: ' . $res -> status_line  if  ( $self ->{ debug
});  709          print  $fh $res -> content ;  712          close ( $fh )  or die  $! ;  715          carp 
'Success, pem certificate in ' . $self ->{ domain
}{ cert
}  if  ( $self ->{ debug
});  722  # OpenSSL configuration file.  723  # This is mostly being used for generation of certificate requests.  730  distinguished_name      
=  req_distinguished_name
 731  # The extentions to add to the self signed cert  732  x509_extensions 
=  v3_ca
 733  # The extensions to add to a certificate request  734  req_extensions 
=  v3_req
 736  # This sets a mask for permitted string types. There are several options.   737  # utf8only: only UTF8Strings (PKIX recommendation after 2004).  738  # WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings.  739  string_mask 
=  utf8only
 741  [  req_distinguished_name 
]  743  stateOrProvinceName             
=  State 
or  Province Name
 744  localityName                    
=  Locality Name
 745  organizationName                
=  Organization Name
 746  organizationalUnitName          
=  Organizational Unit Name
 747  commonName                      
=  __COMMON_NAME__
 748  emailAddress                    
=  __EMAIL_ADDRESS__
 751  basicConstraints 
=  CA
: false
 752  keyUsage 
=  nonRepudiation
,  digitalSignature
,  keyEncipherment
 753  subjectAltName 
=  email
: move
 754  subjectAltName 
=  @alt_names  757  subjectKeyIdentifier 
=  hash
 758  authorityKeyIdentifier 
=  keyid
: always
, issuer
 759  basicConstraints 
=  CA
: true