]>
Raphaël G. Git Repositories - acme/blob - Acme.pm 
db1c6d4c6ca1e0cf15209ea26b9ad3dc58a583e2
   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 <acmepl@rapsys.eu>   27  our  @ISA  =  qw(Exporter) ;   28  our  @EXPORT_OK  =  qw(DS CERT_DIR KEY_DIR REQUEST_CSR ACCOUNT_KEY SERVER_KEY SERVER_CRT CONFIG) ;   31  use  Carp 
qw(carp confess) ;   32  use  Date
:: Parse 
qw(str2time) ;   33  use  Digest
:: SHA 
qw(sha256_base64) ;   35  use  File
:: Path 
qw(make_path) ;   36  use  File
:: Slurp 
qw(read_file write_file) ;   37  use  File
:: Temp
;  # qw( :seekable );   38  use  IPC
:: System
:: Simple 
qw(capturex) ;   39  use  JSON 
qw(from_json to_json) ;   41  use  MIME
:: Base64 
qw(encode_base64url encode_base64) ;   43  use  POSIX 
qw(EXIT_FAILURE) ;   47  #XXX: see https://letsencrypt.github.io/acme-spec/ (probably based on https://ietf-wg-acme.github.io/acme/)   48  #XXX: see jwk rfc http://www.rfc-editor.org/rfc/rfc7517.txt   49  #XXX: see javascript implementation https://github.com/diafygi/gethttpsforfree/blob/gh-pages/js/index.js   56          # Directory for certificates   62          # Directory for pending cache   63          PENDING_DIR 
=>  'pending' ,   65          # Request certificate file name   66          REQUEST_CSR 
=>  'request.der' ,   68          # Account key file name   69          ACCOUNT_KEY 
=>  'account.pem' ,   72          SERVER_KEY 
=>  'server.pem' ,   74          # Server public certificate   75          SERVER_CRT 
=>  'server.crt' ,   84          ACME_CERT 
=>  'https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem' ,   85          ACME_DIR 
=>  'https://acme-staging.api.letsencrypt.org/directory' ,   86          ACME_PROD_DIR 
=>  'https://acme-v01.api.letsencrypt.org/directory' ,   87          ACME_TERMS 
=>  'https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf' ,   93          CONFIG 
=>  '/etc/acmepl/config'  103  #XXX: tie to Tie::IxHash to keep a stable ordering of hash keys  112  #                       kty => uc(KEY_TYPE),  117  #       thumbprint => undef  119  tie
( our  %jwk ,  'Tie::IxHash' ,  pubkey 
=>  undef ,  jwk 
=>  undef ,  thumbprint 
=>  undef );  120  tie
(%{ $jwk { jwk
}},  'Tie::IxHash' ,  alg 
=>  'RS256' ,  jwk 
=>  undef );  121  #XXX: strict ordering only really needed here for thumbprint sha256 digest  122  tie
(%{ $jwk { jwk
}{ jwk
}},  'Tie::IxHash' ,  e 
=>  undef ,  kty 
=>  uc ( KEY_TYPE
),  n 
=>  undef );  127          my  ( $class ,  $mail ,  $debug ,  $prod ,  @domains ) =  @_ ;  132          # Link self to package  133          bless ( $self ,  $class );  136          $self ->{ debug
} =  $debug ;  139          $self ->{ prod
} =  $prod ;  141          # Add extra check to mail validity  142          #XXX: mxcheck fail if there is only a A record on the domain  143          my  $ev  =  Email
:: Valid-
> new (- fqdn 
=>  1 , - tldcheck 
=>  1 , - mxcheck 
=>  1 );  145          # Show error if check fail  146          if  (!  defined  $ev -> address ( $mail )) {  147                  map  {  carp 
'failed check: ' . $_  if  ( $self ->{ debug
}) }  $ev -> details ();  148                  confess 
'Email::Valid->address failed' ;  152          $self ->{ mail
} =  $mail ;  155          my  $res  =  new Net
:: DNS
:: Resolver
();  162                  unless  (( $tld ) =  $_  =~  m/\.(\w+)$/ ) {  163                          confess 
$_ . ' \' s tld extraction failed' ;  166                  # Check if tld exists  167                  unless ( Net
:: Domain
:: TLD
:: tld_exists
( $tld )) {  168                          confess 
$tld . ' tld from ' . $_ . ' don \' t exists' ;  171                  # Check if we get dns answer  172                  #XXX: only search A type because letsencrypt don't support ipv6 (AAAA) yet  173                  unless ( my  $rep  =  $res -> search ( $_ ,  'A' )) {  174                          confess 
'search A record for ' . $_ . ' failed' ;  176                          unless  ( scalar map  {  $_ -> type  eq  'A'  ?  1  : (); }  $rep -> answer ) {  177                                  confess 
'search recursively A record for ' . $_ . ' failed' ;  183          @{ $self ->{ domains
}} =  @domains ;  185          # Return class reference  189  # Prepare environement  194          make_path
( CERT_DIR
,  KEY_DIR
,  PENDING_DIR
. '/' . $self ->{ mail
}. '.' .( $self ->{ prod
} ?  'prod'  :  'staging' ), { error 
=>  \
my  $err });  197                          my  ( $file ,  $msg ) =  %$_ ;  198                          carp 
( $file  eq  ''  ?  ''  :  $file . ': ' ). $msg  if  ( $self ->{ debug
});  200                  confess 
'make_path failed' ;  204          $ua  =  LWP
:: UserAgent-
> new ;  205          $ua -> agent ( __PACKAGE__
. '/' . VERSION
)  211          open ( $_stderr ,  '>&STDERR' )  or die  $! ;  213          close ( STDERR
)  or die  $! ;  215          open ( STDERR
,  '>' ,  '/dev/null' )  or die  $! ;  223          open ( STDERR
,  '>&' ,  $_stderr )  or die  $! ;  226  # Generate required keys  230          # Generate account and server key if required  232                  # Check key existence  237                          #XXX: we drop stderr here because openssl can't be quiet on this command  238                          capturex
( 'openssl' , ( 'genrsa' ,  '-out' ,  $_ ,  KEY_SIZE
));  242          } ( KEY_DIR
. DS
. ACCOUNT_KEY
,  KEY_DIR
. DS
. SERVER_KEY
);  244          # Extract modulus and publicExponent jwk  245          #XXX: same here we tie to keep ordering  246          tie
(%{ $self ->{ account
}},  'Tie::IxHash' ,  %jwk );  248                  if  ( /^Modulus=([0-9A-F]+)$/ ) {  249                          # Extract to binary from hex and convert to base64 url  250                          $self ->{ account
}{ jwk
}{ jwk
}{ n
} =  encode_base64url
( pack ( "H*" ,  $1 ) =~  s/^\0+//r );  251                  }  elsif  ( /^publicExponent:\s([0-9]+)\s\(0x[0-1]+\)$/ ) {  252                          # Extract to binary from int, trim leading zeros and convert to base64 url  253                          chomp  ( $self ->{ account
}{ jwk
}{ jwk
}{ e
} =  encode_base64url
( pack ( "N" ,  $1 ) =~  s/^\0+//r ));  255          }  capturex
( 'openssl' , ( 'rsa' ,  '-text' ,  '-in' ,  KEY_DIR
. DS
. ACCOUNT_KEY
,  '-noout' ,  '-modulus' ));  259          # Extract account public key  260          $self ->{ account
}{ pubkey
} =  join ( '' ,  map  {  chomp ;  $_ ; }  capturex
( 'openssl' , ( 'rsa' ,  '-in' ,  KEY_DIR
. DS
. ACCOUNT_KEY
,  '-pubout' )));  265          #XXX: convert base64 to base64 url  266          $self ->{ account
}{ thumbprint
} = ( sha256_base64
( to_json
( $self ->{ account
}{ jwk
}{ jwk
})) =~  s/=+\z//r ) =~  tr
[+/][- _
] r
;  269  # Generate certificate request  273          # Openssl config template  274          my  $oct  =  File
:: Temp-
> new ();  276          # Load template from data  277          map  {  s/__EMAIL_ADDRESS__/$self->{mail}/ ;  s/__COMMON_NAME__/$self->{domains}[0]/ ;  print  $oct $_ ; } < DATA
>;  282          # Append domain names  284          map  {  print  $oct  'DNS.' . $i++ . ' = ' . $_ . " \n " ; } @{ $self ->{ domains
}};  287          capturex
( 'openssl' , ( 'req' ,  '-new' ,  '-outform' ,  'DER' ,  '-key' ,  KEY_DIR
. DS
. SERVER_KEY
,  '-config' ,  $oct -> filename ,  '-out' ,  CERT_DIR
. DS
. REQUEST_CSR
));  301          my  $dir  =  $self ->{ prod
} ?  ACME_PROD_DIR 
:  ACME_DIR
;  304          my  $req  =  HTTP
:: Request-
> new ( GET 
=>  $dir . '?' . $time );  307          my  $res  =  $ua -> request ( $req );  310          unless  ( $res -> is_success ) {  311                  confess 
'GET ' . $dir . '?' . $time . ' failed: ' . $res -> status_line ;  315          $self ->{ nonce
} =  $res -> headers ->{ 'replay-nonce' };  317          # Merge uris in self content  318          %$self  = ( %$self , %{ from_json
( $res -> content )});  323          my  ( $self ,  $uri ,  $payload ) =  @_ ;  326          my  $protected  =  encode_base64url
( to_json
({ nonce 
=>  $self ->{ nonce
}}));  329          $payload  =  encode_base64url
( to_json
( $payload ));  332          my  $stf  =  File
:: Temp-
> new ();  334          # Append protect.payload to stf  335          print  $stf $protected . '.' . $payload ;  340          # Generate digest of stf  341          my  $signature  =  encode_base64url
( join ( '' ,  capturex
( 'openssl' , ( 'dgst' ,  '-sha256' ,  '-binary' ,  '-sign' ,  KEY_DIR
. DS
. ACCOUNT_KEY
,  $stf -> filename ))) =~  s/^\0+//r );  344          my  $req  =  HTTP
:: Request-
> new ( POST 
=>  $uri );  346          # Set new-reg request content  347          $req -> content ( to_json
({  348                  header 
=>  $self ->{ account
}{ jwk
},  349                  protected 
=>  $protected ,  351                  signature 
=>  $signature  355          my  $res  =  $ua -> request ( $req );  358          if  ( defined  $res -> headers ->{ 'replay-nonce' }) {  359                  $self ->{ nonce
} =  $res -> headers ->{ 'replay-nonce' };  366  # Resolve dns and check content  367  #XXX: see https://community.centminmod.com/threads/looks-like-letsencrypt-dns-01-is-ready.5845/#12 for example  369          my  ( $self ,  $domain ,  $token ) =  @_ ;  371          # Generate signature from content  372          my  $signature  = (( sha256_base64
( $token . '.' . $self ->{ account
}{ thumbprint
})) =~  s/=+\z//r ) =~  tr
[+/][- _
] r
;  375          $domain  =  '_acme-challenge.' . $domain . '.' ;  378          my  $res  =  new Net
:: DNS
:: Resolver
();  380          # Check if we get dns answer  381          unless ( my  $rep  =  $res -> search ( $domain ,  'TXT' )) {  382                  carp 
'TXT record search for ' . $domain . ' failed'  if  ( $self ->{ debug
});  385                  unless  ( scalar map  {  $_ -> type  eq  'TXT'  &&  $_ -> txtdata  =~  /^$signature$/  ?  1  : (); }  $rep -> answer ) {  386                          carp 
'TXT record recursive search for ' . $domain . ' failed'  if  ( $self ->{ debug
});  394  # Get uri and check content  396          my  ( $self ,  $domain ,  $token ) =  @_ ;  399          my  $req  =  HTTP
:: Request-
> new ( GET 
=>  'http://' . $domain . '/.well-known/acme-challenge/' . $token );  401          # Load config if available  404                  #XXX: use eval to workaround a fatal in from_json  406                          # Check that file exists  409                          ( $config  =  read_file
( CONFIG
)) &&  411                          ( $config  =  from_json
( $config )) &&  413                          $config ->{ thumbprint
}  416                  # Try to write thumbprint  417                  write_file
( $config ->{ thumbprint
},  $self ->{ account
}{ thumbprint
});  421          my  $res  =  $ua -> request ( $req );  424          unless  ( $res -> is_success ) {  425                  carp 
'GET http://' . $domain . '/.well-known/acme-challenge/' . $token . ' failed: ' . $res -> status_line  if  ( $self ->{ debug
});  429          # Handle invalid content  430          unless ( $res -> content  =~  /^$token.$self->{account}{thumbprint}\s*$/ ) {  431                  carp 
'GET http://' . $domain . '/.well-known/acme-challenge/' . $token . ' content match failed: /^' . $token . '.' . $self ->{ account
}{ thumbprint
}. '\s* $/  !~ ' . $res -> content  if  ( $self ->{ debug
});  440  #XXX: see doc at https://ietf-wg-acme.github.io/acme/#rfc.section.6.3  444          # Post new-reg request  445          #XXX: contact array may contain a tel:+33612345678 for example  446          my  $res  =  $self -> _post ( $self ->{ 'new-reg' }, { resource 
=>  'new-reg' ,  contact 
=> [ 'mailto:' . $self ->{ mail
}],  agreement 
=>  ACME_TERMS
});  449          unless  ( $res -> is_success  ||  $res -> code  eq  409 ) {  450                  confess 
'POST ' . $self ->{ 'new-reg' }. ' failed: ' . $res -> status_line ;  453          # Update mail informations  454          if  ( $res -> code  eq  409 ) {  455                  # Save registration uri  456                  $self ->{ 'reg' } =  $res -> headers ->{ location
};  459                  #XXX: contact array may contain a tel:+33612345678 for example  460                  $res  =  $self -> _post ( $self ->{ 'reg' }, { resource 
=>  'reg' ,  contact 
=> [ 'mailto:' . $self ->{ mail
}]});  463                  unless  ( $res -> is_success ) {  464                          confess 
'POST ' . $self ->{ 'reg' }. ' failed: ' . $res -> status_line ;  473          # Create challenges hash  474          %{ $self ->{ challenges
}} = ();  479          # Create or load auth request for each domain  485                  my  $file  =  PENDING_DIR
. '/' . $self ->{ mail
}. '.' .( $self ->{ prod
} ?  'prod'  :  'staging' ). '/' . $_ ;  487                  # Load auth request content or post a new one  488                  #TODO: add more check on cache file ???  490                          #XXX: use eval to workaround a fatal in from_json  492                                  # Check that file exists  495                                  ( $content  =  read_file
( $file )) &&  497                                  ( $content  =  from_json
( $content ))  499                          } || ( str2time
( $content ->{ expires
}) <=  time ()+ 3600 )  501                          # Post new-authz request  502                          my  $res  =  $self -> _post ( $self ->{ 'new-authz' }, { resource 
=>  'new-authz' ,  identifier 
=> { type 
=>  'dns' ,  value 
=>  $_ },  existing 
=>  'accept' });  505                          unless  ( $res -> is_success ) {  506                                  confess 
'POST ' . $self ->{ 'new-authz' }. ' for ' . $_ . ' failed: ' . $res -> status_line ;  510                          $content  =  from_json
( $res -> content );  513                          unless  ( defined  $content ->{ identifier
}{ value
} &&  $content ->{ identifier
}{ value
}  eq  $_ ) {  514                                  confess 
'domain matching ' . $content ->{ identifier
}{ value
}. ' for ' . $_ . ' failed: ' . $res -> status_line ;  518                          unless  ( $content ->{ status
}  eq  'valid'  or  $content ->{ status
}  eq  'pending' ) {  519                                  confess 
'POST ' . $self ->{ 'new-authz' }. ' for ' . $_ . ' failed: ' . $res -> status_line ;  523                          write_file
( $file ,  to_json
( $content ));  527                  %{ $self ->{ challenges
}{ $_ }} = (  528                          status 
=>  $content ->{ status
},  529                          expires 
=>  $content ->{ expires
},  534                  if  ( $content ->{ status
}  eq  'pending' ) {  535                          # Extract validation data  536                          foreach  my  $challenge  (@{ $content ->{ challenges
}}) {  537                                  # One test already validated this auth request  538                                  if  ( $self ->{ challenges
}{ $_ }{ status
}  eq  'valid' ) {  540                                  }  elsif  ( $challenge ->{ status
}  eq  'valid' ) {  541                                          $self ->{ challenges
}{ $_ }{ status
} =  $challenge ->{ status
};  543                                  }  elsif  ( $challenge ->{ status
}  eq  'pending' ) {  546                                                  ( $challenge ->{ type
} =~  /^http-01$/  and  $self -> _httpCheck ( $_ ,  $challenge ->{ token
}))  or  547                                                  ( $challenge ->{ type
} =~  /^dns-01$/  and  $self -> _dnsCheck ( $_ ,  $challenge ->{ token
}))  549                                                  # Post challenge request  550                                                  my  $res  =  $self -> _post ( $challenge ->{ uri
}, { resource 
=>  'challenge' ,  keyAuthorization 
=>  $challenge ->{ token
}. '.' . $self ->{ account
}{ thumbprint
}});  553                                                  unless  ( $res -> is_success ) {  554                                                          confess 
'POST ' . $challenge ->{ uri
}. ' failed: ' . $res -> status_line ;  558                                                  my  $content  =  from_json
( $res -> content );  561                                                  if  ( $content ->{ status
}  eq  'valid' ) {  562                                                          $self ->{ challenges
}{ $_ }{ status
} =  $content ->{ status
};  563                                                  # Check is still polling  564                                                  }  elsif  ( $content ->{ status
}  eq  'pending' ) {  565                                                          # Add to poll list for later use  566                                                          push (@{ $self ->{ challenges
}{ $_ }{ polls
}}, {  567                                                                  type 
=> ( split ( /-/ ,  $challenge ->{ type
}))[ 0 ],  568                                                                  status 
=>  $content ->{ status
},  569                                                                  poll 
=>  $content ->{ uri
}  575                          # Check if check is challenge still in pending and no polls  576                          if  ( $self ->{ challenges
}{ $_ }{ status
}  eq  'pending'  &&  scalar  @{ $self ->{ challenges
}{ $_ }{ polls
}} ==  0 ) {  577                                  # Loop on all remaining challenges  578                                  foreach  my  $challenge  (@{ $content ->{ challenges
}}) {  579                                          # Display help for http-01 check  580                                          if  ( $challenge ->{ type
}  eq  'http-01' ) {  581                                                  print  STDERR 
'Create URI http://' . $_ . '/.well-known/acme-challenge/' . $challenge ->{ token
}. ' with content ' . $challenge ->{ token
}. '.' . $self ->{ account
}{ thumbprint
}. " \n " ;  582                                          # Display help for dns-01 check  583                                          }  elsif  ( $challenge ->{ type
}  eq  'dns-01' ) {  584                                                  print  STDERR 
'Create TXT record _acme-challenge.' . $_ . '. with value ' .((( sha256_base64
( $challenge ->{ token
}. '.' . $self ->{ account
}{ thumbprint
})) =~  s/=+\z//r ) =~  tr
[+/][- _
] r
). " \n " ;  589          } @{ $self ->{ domains
}};  595          while  (-- $remaining  >=  0  and scalar map  {  $_ ->{ status
}  eq  'valid'  ?  1  : (); }  values  %{ $self ->{ challenges
}}) {  598                  # Poll remaining pending  603                          # Poll remaining polls  606                                  my  $req  =  HTTP
:: Request-
> new ( GET 
=>  $_ ->{ poll
});  609                                  my  $res  =  $ua -> request ( $req );  612                                  unless  ( $res -> is_success ) {  613                                          carp 
'GET ' . $self ->{ challenges
}{ $_ }{ http_challenge
}. ' failed: ' . $res -> status_line  if  ( $self ->{ debug
});  617                                  my  $content  =  from_json
( $res -> content );  620                                  if  ( $content ->{ status
}  ne  'pending' ) {  621                                          $self ->{ challenges
}{ $domain }{ status
} =  $content ->{ status
};  623                          } @{ $self ->{ challenges
}{ $_ }{ polls
}};  624                  }  map  {  $self ->{ challenges
}{ $_ }{ status
}  eq  'pending'  ?  $_  : (); }  keys  %{ $self ->{ challenges
}};  627          # Load config if available  630                  #XXX: use eval to workaround a fatal in from_json  632                          # Check that file exists  635                          ( $config  =  read_file
( CONFIG
)) &&  637                          ( $config  =  from_json
( $config )) &&  639                          $config ->{ thumbprint
}  642                  # Try to write thumbprint  643                  write_file
( $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 ,  '<' ,  CERT_DIR
. DS
. 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 ,  '>' ,  CERT_DIR
. DS
. SERVER_CRT
)  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 ' . CERT_DIR
. DS
. SERVER_CRT 
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