]>
Raphaël G. Git Repositories - acme/blob - Acme.pm 
acffe8d9d23aab42beb660349febb667505a4ee5
   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) ;   37  use  Digest
:: SHA 
qw(sha256_base64) ;   39  use  File
:: Copy 
qw(copy) ;   40  use  File
:: Path 
qw(make_path) ;   41  use  File
:: Slurp 
qw(read_file write_file) ;   42  use  File
:: stat  qw(stat) ;   43  use  File
:: Temp
;  # qw( :seekable );   44  use  IPC
:: System
:: Simple 
qw(capturex) ;   45  use  JSON 
qw(from_json to_json) ;   47  use  MIME
:: Base64 
qw(encode_base64url encode_base64) ;   49  use  POSIX 
qw(EXIT_FAILURE) ;   53  #XXX: see https://letsencrypt.github.io/acme-spec/ (probably based on https://ietf-wg-acme.github.io/acme/)   54  #XXX: see jwk rfc http://www.rfc-editor.org/rfc/rfc7517.txt   55  #XXX: see javascript implementation https://github.com/diafygi/gethttpsforfree/blob/gh-pages/js/index.js   59          # Request certificate file name   60          REQUEST_CSR 
=>  'request.der' ,   69          ACME_CERT 
=>  'https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem' ,   70          ACME_DIR 
=>  'https://acme-staging.api.letsencrypt.org/directory' ,   71          ACME_PROD_DIR 
=>  'https://acme-v01.api.letsencrypt.org/directory' ,   84  #XXX: tie to Tie::IxHash to keep a stable ordering of hash keys   93  #                       kty => uc(KEY_TYPE),  100  tie
( our  %jwk ,  'Tie::IxHash' ,  pubkey 
=>  undef ,  jwk 
=>  undef ,  thumbprint 
=>  undef );  101  tie
(%{ $jwk { jwk
}},  'Tie::IxHash' ,  alg 
=>  'RS256' ,  jwk 
=>  undef );  102  #XXX: strict ordering only really needed here for thumbprint sha256 digest  103  tie
(%{ $jwk { jwk
}{ jwk
}},  'Tie::IxHash' ,  e 
=>  undef ,  kty 
=>  uc ( KEY_TYPE
),  n 
=>  undef );  108          my  ( $class ,  $debug ,  $domain ,  $config ) =  @_ ;  113          # Link self to package  114          bless ( $self ,  $class );  117          $self ->{ debug
} =  $debug ;  120          $self ->{ domain
} =  $domain ;  123          $self ->{ config
} =  $config ;  126          @{ $self ->{ domains
}} = ( $domain ->{ domain
}, @{ $domain ->{ domains
}});  128          # Add extra check to mail validity  129          #XXX: mxcheck fail if there is only a A record on the domain  130          my  $ev  =  Email
:: Valid-
> new (- fqdn 
=>  1 , - tldcheck 
=>  1 , - mxcheck 
=>  1 );  132          # Show error if check fail  133          if  (!  defined  $ev -> address ( $self ->{ domain
}{ mail
})) {  134                  map  {  carp 
'failed check: ' . $_  if  ( $self ->{ debug
}) }  $ev -> details ();  135                  confess 
'Email::Valid->address failed' ;  139          $self ->{ mail
} =  $self ->{ domain
}{ mail
};  142          my  $res  =  new Net
:: DNS
:: Resolver
();  149                  unless  (( $tld ) =  $_  =~  m/\.(\w+)$/ ) {  150                          confess 
$_ . ' \' s tld extraction failed' ;  153                  # Check if tld exists  154                  unless ( Net
:: Domain
:: TLD
:: tld_exists
( $tld )) {  155                          confess 
$tld . ' tld from ' . $_ . ' don \' t exists' ;  158                  # Check if we get dns answer  159                  #XXX: only search A type because letsencrypt don't support ipv6 (AAAA) yet  160                  unless ( my  $rep  =  $res -> search ( $_ ,  'A' )) {  161                          confess 
'search A record for ' . $_ . ' failed' ;  163                          unless  ( scalar map  {  $_ -> type  eq  'A'  ?  1  : (); }  $rep -> answer ) {  164                                  confess 
'search recursively A record for ' . $_ . ' failed' ;  167          } @{ $self ->{ domains
}};  169          # Return class reference  173  # Prepare environement  177          # Extract cert directory and filename  178          my  ( $certFile ,  $certDir ) =  File
:: Spec-
> splitpath ( $self ->{ domain
}{ cert
});  180          # Extract key directory and filename  181          my  ( $keyFile ,  $keyDir ) =  File
:: Spec-
> splitpath ( $self ->{ domain
}{ key
});  183          # Extract account directory and filename  184          my  ( $accountFile ,  $accountDir ) =  File
:: Spec-
> splitpath ( $self ->{ domain
}{ account
});  188                  make_path
( $certDir ,  $keyDir ,  $accountDir ,  $self ->{ config
}{ pending
}. '/' . $self ->{ mail
}. '.' .( $self ->{ domain
}{ prod
} ?  'prod'  :  'staging' ), { error 
=>  \
my  $err });  191                                  my  ( $file ,  $msg ) =  %$_ ;  192                                  carp 
( $file  eq  ''  ?  ''  :  $file . ': ' ). $msg  if  ( $self ->{ debug
});  194                          confess 
'make_path failed' ;  199          $ua  =  LWP
:: UserAgent-
> new ;  200          $ua -> agent ( __PACKAGE__
. '/' . VERSION
);  202          # Check that certificate is writable  203          unless  (- w 
$certDir  || - w 
$self ->{ domain
}{ cert
}) {  204                  confess
( 'Directory ' . $certDir . ' or file ' . $self ->{ domain
}{ cert
}. ' must be writable: ' . $! );  207          # Check that key is writable  208          unless  (- r 
$self ->{ domain
}{ key
} || - w 
$keyDir ) {  209                  confess
( 'File ' . $self ->{ domain
}{ key
}. ' must be readable or directory ' . $keyDir . ' must be writable: ' . $! );  212          # Check that account is writable  213          unless  (- r 
$self ->{ domain
}{ account
} || - w 
$accountDir ) {  214                  confess
( 'File ' . $self ->{ domain
}{ account
}. ' must be readable or directory ' . $accountDir . ' must be writable: ' . $! );  217          # Backup old certificate if possible  218          if  (- w 
$certDir  && - f 
$self ->{ domain
}{ cert
}) {  219                  my  ( $dt ,  $suffix ) =  undef ;  221                  # Extract datetime suffix  222                  $suffix  = ( $dt  =  DateTime-
> from_epoch ( epoch 
=>  stat ( $self ->{ domain
}{ cert
})-> mtime ))-> ymd ( '' ). $dt -> hms ( '' );  224                  # Rename old certificate  225                  unless ( copy
( $self ->{ domain
}{ cert
},  $self ->{ domain
}{ cert
}. '.' . $suffix )) {  226                          carp
( 'Copy ' . $self ->{ domain
}{ cert
}. ' to ' . $self ->{ domain
}{ cert
}. '.' . $suffix . ' failed: ' . $! );  234          open ( $_stderr ,  '>&STDERR' )  or die  $! ;  236          close ( STDERR
)  or die  $! ;  238          open ( STDERR
,  '>' ,  '/dev/null' )  or die  $! ;  246          open ( STDERR
,  '>&' ,  $_stderr )  or die  $! ;  249  # Generate required keys  253          # Generate account and server key if required  255                  # Check key existence  260                          #XXX: we drop stderr here because openssl can't be quiet on this command  261                          capturex
( 'openssl' , ( 'genrsa' ,  '-out' ,  $_ ,  KEY_SIZE
));  265          } ( $self ->{ domain
}{ account
},  $self ->{ domain
}{ key
});  267          # Extract modulus and publicExponent jwk  268          #XXX: same here we tie to keep ordering  269          tie
(%{ $self ->{ account
}},  'Tie::IxHash' ,  %jwk );  271                  if  ( /^Modulus=([0-9A-F]+)$/ ) {  272                          # Extract to binary from hex and convert to base64 url  273                          $self ->{ account
}{ jwk
}{ jwk
}{ n
} =  encode_base64url
( pack ( "H*" ,  $1 ) =~  s/^\0+//r );  274                  }  elsif  ( /^publicExponent:\s([0-9]+)\s\(0x[0-1]+\)$/ ) {  275                          # Extract to binary from int, trim leading zeros and convert to base64 url  276                          chomp  ( $self ->{ account
}{ jwk
}{ jwk
}{ e
} =  encode_base64url
( pack ( "N" ,  $1 ) =~  s/^\0+//r ));  278          }  capturex
( 'openssl' , ( 'rsa' ,  '-text' ,  '-in' ,  $self ->{ domain
}{ account
},  '-noout' ,  '-modulus' ));  282          # Extract account public key  283          $self ->{ account
}{ pubkey
} =  join ( '' ,  map  {  chomp ;  $_ ; }  capturex
( 'openssl' , ( 'rsa' ,  '-in' ,  $self ->{ domain
}{ account
},  '-pubout' )));  288          #XXX: convert base64 to base64 url  289          $self ->{ account
}{ thumbprint
} = ( sha256_base64
( to_json
( $self ->{ account
}{ jwk
}{ jwk
})) =~  s/=+\z//r ) =~  tr
[+/][- _
] r
;  292  # Generate certificate request  296          # Openssl config template  297          my  $oct  =  File
:: Temp-
> new ();  299          # Save data start position  302          # Load template from data  303          map  {  s/__EMAIL_ADDRESS__/$self->{mail}/ ;  s/__COMMON_NAME__/$self->{domains}[0]/ ;  print  $oct $_ ; } < DATA
>;  308          # Append domain names  310          map  {  print  $oct  'DNS.' . $i++ . ' = ' . $_ . " \n " ; } @{ $self ->{ domains
}};  313          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
));  327          my  $dir  =  $self ->{ domain
}{ prod
} ?  ACME_PROD_DIR 
:  ACME_DIR
;  330          my  $req  =  HTTP
:: Request-
> new ( GET 
=>  $dir . '?' . $time );  333          my  $res  =  $ua -> request ( $req );  336          unless  ( $res -> is_success ) {  337                  confess 
'GET ' . $dir . '?' . $time . ' failed: ' . $res -> status_line ;  341          $self ->{ nonce
} =  $res -> headers ->{ 'replay-nonce' };  343          # Merge uris in self content  344          %$self  = ( %$self , %{ from_json
( $res -> content )});  349          my  ( $self ,  $uri ,  $payload ) =  @_ ;  352          my  $protected  =  encode_base64url
( to_json
({ nonce 
=>  $self ->{ nonce
}}));  355          $payload  =  encode_base64url
( to_json
( $payload ));  358          my  $stf  =  File
:: Temp-
> new ();  360          # Append protect.payload to stf  361          print  $stf $protected . '.' . $payload ;  366          # Generate digest of stf  367          my  $signature  =  encode_base64url
( join ( '' ,  capturex
( 'openssl' , ( 'dgst' ,  '-sha256' ,  '-binary' ,  '-sign' ,  $self ->{ domain
}{ account
},  $stf -> filename ))) =~  s/^\0+//r );  370          my  $req  =  HTTP
:: Request-
> new ( POST 
=>  $uri );  372          # Set new-reg request content  373          $req -> content ( to_json
({  374                  header 
=>  $self ->{ account
}{ jwk
},  375                  protected 
=>  $protected ,  377                  signature 
=>  $signature  381          my  $res  =  $ua -> request ( $req );  384          if  ( defined  $res -> headers ->{ 'replay-nonce' }) {  385                  $self ->{ nonce
} =  $res -> headers ->{ 'replay-nonce' };  392  # Resolve dns and check content  393  #XXX: see https://community.centminmod.com/threads/looks-like-letsencrypt-dns-01-is-ready.5845/#12 for example  395          my  ( $self ,  $domain ,  $token ) =  @_ ;  397          # Generate signature from content  398          my  $signature  = (( sha256_base64
( $token . '.' . $self ->{ account
}{ thumbprint
})) =~  s/=+\z//r ) =~  tr
[+/][- _
] r
;  401          $domain  =  '_acme-challenge.' . $domain . '.' ;  404          my  $res  =  new Net
:: DNS
:: Resolver
();  406          # Check if we get dns answer  407          unless ( my  $rep  =  $res -> search ( $domain ,  'TXT' )) {  408                  carp 
'TXT record search for ' . $domain . ' failed'  if  ( $self ->{ debug
});  411                  unless  ( scalar map  {  $_ -> type  eq  'TXT'  &&  $_ -> txtdata  =~  /^$signature$/  ?  1  : (); }  $rep -> answer ) {  412                          carp 
'TXT record recursive search for ' . $domain . ' failed'  if  ( $self ->{ debug
});  420  # Get uri and check content  422          my  ( $self ,  $domain ,  $token ) =  @_ ;  425          my  $req  =  HTTP
:: Request-
> new ( GET 
=>  'http://' . $domain . '/.well-known/acme-challenge/' . $token );  427          # Check if thumbprint is writeable  428          if  (- w 
$self ->{ config
}{ thumbprint
}) {  429                  # Try to write thumbprint  430                  write_file
( $self ->{ config
}{ thumbprint
},  $self ->{ account
}{ thumbprint
});  434          my  $res  =  $ua -> request ( $req );  437          unless  ( $res -> is_success ) {  438                  carp 
'GET http://' . $domain . '/.well-known/acme-challenge/' . $token . ' failed: ' . $res -> status_line  if  ( $self ->{ debug
});  442          # Handle invalid content  443          unless ( $res -> content  =~  /^$token.$self->{account}{thumbprint}\s*$/ ) {  444                  carp 
'GET http://' . $domain . '/.well-known/acme-challenge/' . $token . ' content match failed: /^' . $token . '.' . $self ->{ account
}{ thumbprint
}. '\s* $/  !~ ' . $res -> content  if  ( $self ->{ debug
});  453  #XXX: see doc at https://ietf-wg-acme.github.io/acme/#rfc.section.6.3  457          # Post new-reg request  458          #XXX: contact array may contain a tel:+33612345678 for example  459          my  $res  =  $self -> _post ( $self ->{ 'new-reg' }, { resource 
=>  'new-reg' ,  contact 
=> [ 'mailto:' . $self ->{ mail
}],  agreement 
=>  $self ->{ term
}});  462          unless  ( $res -> is_success  ||  $res -> code  eq  409 ) {  463                  confess 
'POST ' . $self ->{ 'new-reg' }. ' failed: ' . $res -> status_line ;  466          # Update mail informations  467          if  ( $res -> code  eq  409 ) {  468                  # Save registration uri  469                  $self ->{ 'reg' } =  $res -> headers ->{ location
};  472                  #XXX: contact array may contain a tel:+33612345678 for example  473                  $res  =  $self -> _post ( $self ->{ 'reg' }, { resource 
=>  'reg' ,  contact 
=> [ 'mailto:' . $self ->{ mail
}]});  476                  unless  ( $res -> is_success ) {  477                          confess 
'POST ' . $self ->{ 'reg' }. ' failed: ' . $res -> status_line ;  486          # Create challenges hash  487          %{ $self ->{ challenges
}} = ();  492          # Create or load auth request for each domain  498                  my  $file  =  $self ->{ config
}{ pending
}. '/' . $self ->{ mail
}. '.' .( $self ->{ domain
}{ prod
} ?  'prod'  :  'staging' ). '/' . $_ ;  500                  # Load auth request content or post a new one  501                  #TODO: add more check on cache file ???  503                          #XXX: use eval to workaround a fatal in from_json  505                                  # Check that file exists  508                                  ( $content  =  read_file
( $file )) &&  510                                  ( $content  =  from_json
( $content ))  512                          } || ( str2time
( $content ->{ expires
}) <=  time ()+ 3600 )  514                          # Post new-authz request  515                          my  $res  =  $self -> _post ( $self ->{ 'new-authz' }, { resource 
=>  'new-authz' ,  identifier 
=> { type 
=>  'dns' ,  value 
=>  $_ },  existing 
=>  'accept' });  518                          unless  ( $res -> is_success ) {  519                                  confess 
'POST ' . $self ->{ 'new-authz' }. ' for ' . $_ . ' failed: ' . $res -> status_line ;  523                          $content  =  from_json
( $res -> content );  526                          unless  ( defined  $content ->{ identifier
}{ value
} &&  $content ->{ identifier
}{ value
}  eq  $_ ) {  527                                  confess 
'domain matching ' . $content ->{ identifier
}{ value
}. ' for ' . $_ . ' failed: ' . $res -> status_line ;  531                          unless  ( $content ->{ status
}  eq  'valid'  or  $content ->{ status
}  eq  'pending' ) {  532                                  confess 
'POST ' . $self ->{ 'new-authz' }. ' for ' . $_ . ' failed: ' . $res -> status_line ;  536                          write_file
( $file ,  to_json
( $content ));  540                  %{ $self ->{ challenges
}{ $_ }} = (  541                          status 
=>  $content ->{ status
},  542                          expires 
=>  $content ->{ expires
},  547                  if  ( $content ->{ status
}  eq  'pending' ) {  548                          # Extract validation data  549                          foreach  my  $challenge  (@{ $content ->{ challenges
}}) {  550                                  # One test already validated this auth request  551                                  if  ( $self ->{ challenges
}{ $_ }{ status
}  eq  'valid' ) {  553                                  }  elsif  ( $challenge ->{ status
}  eq  'valid' ) {  554                                          $self ->{ challenges
}{ $_ }{ status
} =  $challenge ->{ status
};  556                                  }  elsif  ( $challenge ->{ status
}  eq  'pending' ) {  559                                                  ( $challenge ->{ type
} =~  /^http-01$/  and  $self -> _httpCheck ( $_ ,  $challenge ->{ token
}))  or  560                                                  ( $challenge ->{ type
} =~  /^dns-01$/  and  $self -> _dnsCheck ( $_ ,  $challenge ->{ token
}))  562                                                  # Post challenge request  563                                                  my  $res  =  $self -> _post ( $challenge ->{ uri
}, { resource 
=>  'challenge' ,  keyAuthorization 
=>  $challenge ->{ token
}. '.' . $self ->{ account
}{ thumbprint
}});  566                                                  unless  ( $res -> is_success ) {  567                                                          confess 
'POST ' . $challenge ->{ uri
}. ' failed: ' . $res -> status_line ;  571                                                  my  $content  =  from_json
( $res -> content );  574                                                  if  ( $content ->{ status
}  eq  'valid' ) {  575                                                          $self ->{ challenges
}{ $_ }{ status
} =  $content ->{ status
};  576                                                  # Check is still polling  577                                                  }  elsif  ( $content ->{ status
}  eq  'pending' ) {  578                                                          # Add to poll list for later use  579                                                          push (@{ $self ->{ challenges
}{ $_ }{ polls
}}, {  580                                                                  type 
=> ( split ( /-/ ,  $challenge ->{ type
}))[ 0 ],  581                                                                  status 
=>  $content ->{ status
},  582                                                                  poll 
=>  $content ->{ uri
}  588                          # Check if check is challenge still in pending and no polls  589                          if  ( $self ->{ challenges
}{ $_ }{ status
}  eq  'pending'  &&  scalar  @{ $self ->{ challenges
}{ $_ }{ polls
}} ==  0 ) {  590                                  # Loop on all remaining challenges  591                                  foreach  my  $challenge  (@{ $content ->{ challenges
}}) {  592                                          # Display help for http-01 check  593                                          if  ( $challenge ->{ type
}  eq  'http-01' ) {  594                                                  print  STDERR 
'Create URI http://' . $_ . '/.well-known/acme-challenge/' . $challenge ->{ token
}. ' with content ' . $challenge ->{ token
}. '.' . $self ->{ account
}{ thumbprint
}. " \n " ;  595                                          # Display help for dns-01 check  596                                          }  elsif  ( $challenge ->{ type
}  eq  'dns-01' ) {  597                                                  print  STDERR 
'Create TXT record _acme-challenge.' . $_ . '. with value ' .((( sha256_base64
( $challenge ->{ token
}. '.' . $self ->{ account
}{ thumbprint
})) =~  s/=+\z//r ) =~  tr
[+/][- _
] r
). " \n " ;  602          } @{ $self ->{ domains
}};  608          while  (-- $remaining  >=  0  and scalar map  {  $_ ->{ status
}  eq  'pending'  ?  1  : (); }  values  %{ $self ->{ challenges
}}) {  611                  # Poll remaining pending  616                          # Poll remaining polls  619                                  my  $req  =  HTTP
:: Request-
> new ( GET 
=>  $_ ->{ poll
});  622                                  my  $res  =  $ua -> request ( $req );  625                                  unless  ( $res -> is_success ) {  626                                          carp 
'GET ' . $self ->{ challenges
}{ $_ }{ http_challenge
}. ' failed: ' . $res -> status_line  if  ( $self ->{ debug
});  630                                  my  $content  =  from_json
( $res -> content );  633                                  if  ( $content ->{ status
}  ne  'pending' ) {  634                                          $self ->{ challenges
}{ $domain }{ status
} =  $content ->{ status
};  636                          } @{ $self ->{ challenges
}{ $_ }{ polls
}};  637                  }  map  {  $self ->{ challenges
}{ $_ }{ status
}  eq  'pending'  ?  $_  : (); }  keys  %{ $self ->{ challenges
}};  640          # Check if thumbprint is writeable  641          if  (- w 
$self ->{ config
}{ thumbprint
}) {  642                  # Try to write thumbprint  643                  write_file
( $self ->{ config
}{ thumbprint
},  '' );  646          # Stop here with remaining chanllenge  647          if  ( scalar map  { !  defined  $_ ->{ status
}  or  $_ ->{ status
}  ne  'valid'  ?  1  : (); }  values  %{ $self ->{ challenges
}}) {  648                  # Deactivate all activated domains   649                  #XXX: not implemented by letsencrypt  651                  #       # Post deactivation request  652                  #       my $res = $self->_post($self->{challenges}{$_}{http_uri}, {resource => 'authz', status => 'deactivated'});  654                  #       unless ($res->is_success) {  655                  #               confess 'POST '.$self->{challenges}{$_}{http_uri}.' failed: '.$res->status_line;  657                  #} map { $self->{challenges}{$_}{status} eq 'valid' ? $_ : () } keys %{$self->{challenges}};  659                  # Stop here as a domain of csr list failed authorization  660                  if  ( $self ->{ debug
}) {  661                          my  @domains  =  map  { !  defined  $self ->{ challenges
}{ $_ }{ status
}  or  $self ->{ challenges
}{ $_ }{ status
}  ne  'valid'  ?  $_  : (); }  keys  %{ $self ->{ challenges
}};  662                          confess 
'Fix the challenge' .( scalar  @domains  >  1 ? 's' : '' ). ' for domain' .( scalar  @domains  >  1 ? 's' : '' ). ': ' . join ( ', ' ,  @domains );  674          open ( my  $fh ,  '<' ,  $self ->{ config
}{ pending
}. '/' . $self ->{ mail
}. '.' .( $self ->{ domain
}{ prod
} ?  'prod'  :  'staging' ). '/' . REQUEST_CSR
)  or die  $! ;  677          my  $csr  =  encode_base64url
( join ( '' , < $fh >) =~  s/^\0+//r );  680          close ( $fh )  or die  $! ;  682          # Post certificate request  683          my  $res  =  $self -> _post ( $self ->{ 'new-cert' }, { resource 
=>  'new-cert' ,  csr 
=>  $csr });  686          unless  ( $res -> is_success ) {  687                  confess 
'POST ' . $self ->{ 'new-cert' }. ' failed: ' . $res -> status_line ;  691          open ( $fh ,  '>' ,  $self ->{ domain
}{ cert
})  or die  $! ;  694          print  $fh  '-----BEGIN CERTIFICATE-----' . " \n " . encode_base64
( $res -> content ). '-----END CERTIFICATE-----' . " \n " ;  697          my  $req  =  HTTP
:: Request-
> new ( GET 
=>  ACME_CERT
);  700          $res  =  $ua -> request ( $req );  703          unless  ( $res -> is_success ) {  704                  carp 
'GET ' . ACME_CERT
. ' failed: ' . $res -> status_line  if  ( $self ->{ debug
});  708          print  $fh $res -> content ;  711          close ( $fh )  or die  $! ;  714          carp 
'Success, pem certificate in ' . $self ->{ domain
}{ cert
}  if  ( $self ->{ debug
});  721  # OpenSSL configuration file.  722  # This is mostly being used for generation of certificate requests.  729  distinguished_name      
=  req_distinguished_name
 730  # The extentions to add to the self signed cert  731  x509_extensions 
=  v3_ca
 732  # The extensions to add to a certificate request  733  req_extensions 
=  v3_req
 735  # This sets a mask for permitted string types. There are several options.   736  # utf8only: only UTF8Strings (PKIX recommendation after 2004).  737  # WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings.  738  string_mask 
=  utf8only
 740  [  req_distinguished_name 
]  742  stateOrProvinceName             
=  State 
or  Province Name
 743  localityName                    
=  Locality Name
 744  organizationName                
=  Organization Name
 745  organizationalUnitName          
=  Organizational Unit Name
 746  commonName                      
=  __COMMON_NAME__
 747  emailAddress                    
=  __EMAIL_ADDRESS__
 750  basicConstraints 
=  CA
: false
 751  keyUsage 
=  nonRepudiation
,  digitalSignature
,  keyEncipherment
 752  subjectAltName 
=  email
: move
 753  subjectAltName 
=  @alt_names  756  subjectKeyIdentifier 
=  hash
 757  authorityKeyIdentifier 
=  keyid
: always
, issuer
 758  basicConstraints 
=  CA
: true