]> Raphaƫl G. Git Repositories - acme/blob - acme.pm
Fix expiration detection of authz
[acme] / acme.pm
1 # acme package
2 package acme;
3
4 # Best practice
5 use strict;
6 use warnings;
7
8 # Symbol export
9 use Exporter;
10 our @ISA = qw(Exporter);
11 our @EXPORT_OK = qw(DS CERT_DIR KEY_DIR REQUEST_CSR ACCOUNT_KEY SERVER_KEY SERVER_CRT CONFIG);
12
13 # Load dependancies
14 use Carp qw(carp confess);
15 use Data::Dumper;
16 use Date::Parse qw(str2time);
17 use Digest::SHA qw(sha256_base64);
18 use Email::Valid;
19 use File::Path qw(make_path);
20 use File::Slurp qw(read_file write_file);
21 use File::Temp; # qw( :seekable );
22 use IPC::System::Simple qw(capturex);
23 use JSON qw(encode_json decode_json);
24 use LWP;
25 use MIME::Base64 qw(encode_base64url encode_base64);
26 use Net::Domain::TLD;
27 use POSIX qw(EXIT_FAILURE);
28 use Tie::IxHash;
29
30 # Documentation links
31 #XXX: see https://letsencrypt.github.io/acme-spec/ (probably based on https://ietf-wg-acme.github.io/acme/)
32 #XXX: see jwk rfc http://www.rfc-editor.org/rfc/rfc7517.txt
33 #XXX: see javascript implementation https://github.com/diafygi/gethttpsforfree/blob/gh-pages/js/index.js
34
35 # Set constants
36 use constant {
37 # Directory separator
38 DS => '/',
39
40 # Directory for certificates
41 CERT_DIR => 'cert',
42
43 # Directory for keys
44 KEY_DIR => 'key',
45
46 # Directory for pending cache
47 PENDING_DIR => 'pending',
48
49 # Request certificate file name
50 REQUEST_CSR => 'request.der',
51
52 # Account key file name
53 ACCOUNT_KEY => 'account.pem',
54
55 # Server private key
56 SERVER_KEY => 'server.pem',
57
58 # Server public certificate
59 SERVER_CRT => 'server.crt',
60
61 # rsa
62 KEY_TYPE => 'rsa',
63
64 # 2048|4096
65 KEY_SIZE => 4096,
66
67 # Acme infos
68 ACME_CERT => 'https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem',
69 ACME_DIR => 'https://acme-staging.api.letsencrypt.org/directory',
70 ACME_PROD_DIR => 'https://acme-v01.api.letsencrypt.org/directory',
71 ACME_TERMS => 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf',
72
73 # Version
74 VERSION => 'v0.4',
75
76 # Config
77 CONFIG => '/etc/acmepl/config'
78 };
79
80 # User agent object
81 our $ua;
82
83 # Strerr backup
84 our $_stderr;
85
86 # JSON Web Key (JWK)
87 #XXX: tie to Tie::IxHash to keep a stable ordering of hash keys
88 #our %jwk = (
89 # pubkey => undef,
90 # jwk => {
91 # alg => 'RS256',
92 # jwk => {
93 # # Exponent
94 # e => undef,
95 # # Key type
96 # kty => uc(KEY_TYPE),
97 # # Modulus
98 # n => undef
99 # }
100 # },
101 # thumbprint => undef
102 #);
103 tie(our %jwk, 'Tie::IxHash', pubkey => undef, jwk => undef, thumbprint => undef);
104 tie(%{$jwk{jwk}}, 'Tie::IxHash', alg => 'RS256', jwk => undef);
105 #XXX: strict ordering only really needed here for thumbprint sha256 digest
106 tie(%{$jwk{jwk}{jwk}}, 'Tie::IxHash', e => undef, kty => uc(KEY_TYPE), n => undef);
107
108 # Constructor
109 sub new {
110 # Extract params
111 my ($class, $mail, $debug, $prod, @domains) = @_;
112
113 # Create self hash
114 my $self = {};
115
116 # Link self to package
117 bless($self, $class);
118
119 # Save debug
120 $self->{debug} = $debug;
121
122 # Save prod
123 $self->{prod} = $prod;
124
125 # Add extra check to mail validity
126 #XXX: mxcheck fail if there is only a A record on the domain
127 my $ev = Email::Valid->new(-fqdn => 1, -tldcheck => 1, -mxcheck => 1);
128
129 # Show error if check fail
130 if (! defined $ev->address($mail)) {
131 map { carp 'failed check: '.$_ if ($self->{debug}) } $ev->details();
132 confess 'Email::Valid->address failed';
133 }
134
135 # Save mail
136 $self->{mail} = $mail;
137
138 # Create resolver
139 my $res = new Net::DNS::Resolver();
140
141 # Check domains
142 map {
143 my $tld;
144
145 # Extract tld
146 unless (($tld) = $_ =~ m/\.(\w+)$/) {
147 confess $_.'\'s tld extraction failed';
148 }
149
150 # Check if tld exists
151 unless(Net::Domain::TLD::tld_exists($tld)) {
152 confess $tld.' tld from '.$_.' don\'t exists';
153 }
154
155 # Check if we get dns answer
156 #XXX: only search A type because letsencrypt don't support ipv6 (AAAA) yet
157 unless(my $rep = $res->search($_, 'A')) {
158 confess 'search A record for '.$_.' failed';
159 } else {
160 unless (scalar map { $_->type eq 'A' ? 1 : (); } $rep->answer) {
161 confess 'search recursively A record for '.$_.' failed';
162 }
163 }
164 } @domains;
165
166 # Save domains
167 @{$self->{domains}} = @domains;
168
169 # Return class reference
170 return $self;
171 }
172
173 # Prepare environement
174 sub prepare {
175 my ($self) = @_;
176
177 # Create all paths
178 make_path(CERT_DIR, KEY_DIR, PENDING_DIR.'/'.$self->{mail}.'.'.($self->{prod} ? 'prod' : 'staging'), {error => \my $err});
179 if (@$err) {
180 map {
181 my ($file, $msg) = %$_;
182 carp ($file eq '' ? '' : $file.': ').$msg if ($self->{debug});
183 } @$err;
184 confess 'make_path failed';
185 }
186
187 # Create user agent
188 $ua = LWP::UserAgent->new;
189 $ua->agent(__PACKAGE__.'/'.VERSION)
190 }
191
192 # Drop stderr
193 sub _dropStdErr {
194 # Save stderr
195 open($_stderr, '>&STDERR') or die $!;
196 # Close it
197 close(STDERR) or die $!;
198 # Send to /dev/null
199 open(STDERR, '>', '/dev/null') or die $!;
200 }
201
202 # Restore stderr
203 sub _restoreStdErr {
204 # Close stderr
205 close(STDERR);
206 # Open it back
207 open(STDERR, '>&', $_stderr) or die $!;
208 }
209
210 # Generate required keys
211 sub genKeys {
212 my ($self) = @_;
213
214 # Generate account and server key if required
215 map {
216 # Check key existence
217 if (! -f $_) {
218 # Drop stderr
219 _dropStdErr();
220 # Generate key
221 #XXX: we drop stderr here because openssl can't be quiet on this command
222 capturex('openssl', ('genrsa', '-out', $_, KEY_SIZE));
223 # Restore stderr
224 _restoreStdErr();
225 }
226 } (KEY_DIR.DS.ACCOUNT_KEY, KEY_DIR.DS.SERVER_KEY);
227
228 # Extract modulus and publicExponent jwk
229 #XXX: same here we tie to keep ordering
230 tie(%{$self->{account}}, 'Tie::IxHash', %jwk);
231 map {
232 if (/^Modulus=([0-9A-F]+)$/) {
233 # Extract to binary from hex and convert to base64 url
234 $self->{account}{jwk}{jwk}{n} = encode_base64url(pack("H*", $1) =~ s/^\0+//r);
235 } elsif (/^publicExponent:\s([0-9]+)\s\(0x[0-1]+\)$/) {
236 # Extract to binary from int, trim leading zeros and convert to base64 url
237 chomp ($self->{account}{jwk}{jwk}{e} = encode_base64url(pack("N", $1) =~ s/^\0+//r));
238 }
239 } capturex('openssl', ('rsa', '-text', '-in', KEY_DIR.DS.ACCOUNT_KEY, '-noout', '-modulus'));
240
241 # Drop stderr
242 _dropStdErr();
243 # Extract account public key
244 $self->{account}{pubkey} = join('', map { chomp; $_; } capturex('openssl', ('rsa', '-in', KEY_DIR.DS.ACCOUNT_KEY, '-pubout')));
245 # Restore stderr
246 _restoreStdErr();
247
248 # Store thumbprint
249 #XXX: convert base64 to base64 url
250 $self->{account}{thumbprint} = (sha256_base64(encode_json($self->{account}{jwk}{jwk})) =~ s/=+\z//r) =~ tr[+/][-_]r;
251 }
252
253 # Generate certificate request
254 sub genCsr {
255 my ($self) = @_;
256
257 # Openssl config template
258 my $oct = File::Temp->new();
259
260 # Load template from data
261 map { s/__EMAIL_ADDRESS__/$self->{mail}/; s/__COMMON_NAME__/$self->{domains}[0]/; print $oct $_; } <DATA>;
262
263 # Close data
264 close(DATA);
265
266 # Append domain names
267 my $i = 1;
268 map { print $oct 'DNS.'.$i++.' = '.$_."\n"; } @{$self->{domains}};
269
270 # Generate csr
271 capturex('openssl', ('req', '-new', '-outform', 'DER', '-key', KEY_DIR.DS.SERVER_KEY, '-config', $oct->filename, '-out', CERT_DIR.DS.REQUEST_CSR));
272
273 # Close oct
274 close($oct);
275 }
276
277 # Directory call
278 sub directory {
279 my ($self) = @_;
280
281 # Set time
282 my $time = time;
283
284 # Set directory
285 my $dir = $self->{prod} ? ACME_PROD_DIR : ACME_DIR;
286
287 # Create a request
288 my $req = HTTP::Request->new(GET => $dir.'?'.$time);
289
290 # Get request
291 my $res = $ua->request($req);
292
293 # Handle error
294 unless ($res->is_success) {
295 confess 'GET '.$dir.'?'.$time.' failed: '.$res->status_line;
296 }
297
298 # Save nonce
299 $self->{nonce} = $res->headers->{'replay-nonce'};
300
301 # Merge uris in self content
302 %$self = (%$self, %{decode_json($res->content)});
303 }
304
305 # Post request
306 sub _post {
307 my ($self, $uri, $payload) = @_;
308
309 # Protected field
310 my $protected = encode_base64url(encode_json({nonce => $self->{nonce}}));
311
312 # Payload field
313 $payload = encode_base64url(encode_json($payload));
314
315 # Sign temp file
316 my $stf = File::Temp->new();
317
318 # Append protect.payload to stf
319 print $stf $protected.'.'.$payload;
320
321 # Close stf
322 close($stf);
323
324 # Generate digest of stf
325 my $signature = encode_base64url(join('', capturex('openssl', ('dgst', '-sha256', '-binary', '-sign', KEY_DIR.DS.ACCOUNT_KEY, $stf->filename))) =~ s/^\0+//r);
326
327 # Create a request
328 my $req = HTTP::Request->new(POST => $uri);
329
330 # Set new-reg request content
331 $req->content(encode_json({
332 header => $self->{account}{jwk},
333 protected => $protected,
334 payload => $payload,
335 signature => $signature
336 }));
337
338 # Post request
339 my $res = $ua->request($req);
340
341 # Save nonce
342 if (defined $res->headers->{'replay-nonce'}) {
343 $self->{nonce} = $res->headers->{'replay-nonce'};
344 }
345
346 # Return res object
347 return $res;
348 }
349
350 # Resolve dns and check content
351 #XXX: see https://community.centminmod.com/threads/looks-like-letsencrypt-dns-01-is-ready.5845/#12 for example
352 sub _dnsCheck {
353 my ($self, $domain, $token) = @_;
354
355 # Generate signature from content
356 my $signature = ((sha256_base64($token.'.'.$self->{account}{thumbprint})) =~ s/=+\z//r) =~ tr[+/][-_]r;
357
358 # Fix domain
359 $domain = '_acme-challenge.'.$domain.'.';
360
361 # Create resolver
362 my $res = new Net::DNS::Resolver();
363
364 # Check if we get dns answer
365 unless(my $rep = $res->search($domain, 'TXT')) {
366 carp 'TXT record search for '.$domain.' failed' if ($self->{debug});
367 return;
368 } else {
369 unless (scalar map { $_->type eq 'TXT' && $_->txtdata =~ /^$signature$/ ? 1 : (); } $rep->answer) {
370 carp 'TXT record recursive search for '.$domain.' failed' if ($self->{debug});
371 return;
372 }
373 }
374
375 return 1;
376 }
377
378 # Get uri and check content
379 sub _httpCheck {
380 my ($self, $domain, $token) = @_;
381
382 # Create a request
383 my $req = HTTP::Request->new(GET => 'http://'.$domain.'/.well-known/acme-challenge/'.$token);
384
385 # Load config if available
386 my $config = undef;
387 if (
388 #XXX: use eval to workaround a fatal in decode_json
389 defined eval {
390 # Check that file exists
391 -f CONFIG &&
392 # Read it
393 ($config = read_file(CONFIG)) &&
394 # Decode it
395 ($config = decode_json($config)) &&
396 # Check defined
397 $config->{thumbprint}
398 }
399 ) {
400 # Try to write thumbprint
401 write_file($config->{thumbprint}, $self->{account}{thumbprint});
402 }
403
404 # Get request
405 my $res = $ua->request($req);
406
407 # Handle error
408 unless ($res->is_success) {
409 carp 'GET http://'.$domain.'/.well-known/acme-challenge/'.$token.' failed: '.$res->status_line if ($self->{debug});
410 return;
411 }
412
413 # Handle invalid content
414 unless($res->content =~ /^$token.$self->{account}{thumbprint}\s*$/) {
415 carp 'GET http://'.$domain.'/.well-known/acme-challenge/'.$token.' content match failed: /^'.$token.'.'.$self->{account}{thumbprint}.'\s*$/ !~ '.$res->content if ($self->{debug});
416 return;
417 }
418
419 # Return success
420 return 1;
421 }
422
423 # Register account
424 #XXX: see doc at https://ietf-wg-acme.github.io/acme/#rfc.section.6.3
425 sub register {
426 my ($self) = @_;
427
428 # Post new-reg request
429 #XXX: contact array may contain a tel:+33612345678 for example
430 my $res = $self->_post($self->{'new-reg'}, {resource => 'new-reg', contact => ['mailto:'.$self->{mail}], agreement => ACME_TERMS});
431
432 # Handle error
433 unless ($res->is_success || $res->code eq 409) {
434 confess 'POST '.$self->{'new-reg'}.' failed: '.$res->status_line;
435 }
436
437 # Update mail informations
438 if ($res->code eq 409) {
439 # Save registration uri
440 $self->{'reg'} = $res->headers->{location};
441
442 # Post reg request
443 #XXX: contact array may contain a tel:+33612345678 for example
444 $res = $self->_post($self->{'reg'}, {resource => 'reg', contact => ['mailto:'.$self->{mail}]});
445
446 # Handle error
447 unless ($res->is_success) {
448 confess 'POST '.$self->{'reg'}.' failed: '.$res->status_line;
449 }
450 }
451 }
452
453 # Authorize domains
454 sub authorize {
455 my ($self) = @_;
456
457 # Create challenges hash
458 %{$self->{challenges}} = ();
459
460 # Pending list
461 my @pending = ();
462
463 # Create or load auth request for each domain
464 map {
465 # Init content
466 my $content = undef;
467
468 # Init file
469 my $file = PENDING_DIR.'/'.$self->{mail}.'.'.($self->{prod} ? 'prod' : 'staging').'/'.$_;
470
471 # Load auth request content or post a new one
472 #TODO: add more check on cache file ???
473 if (
474 #XXX: use eval to workaround a fatal in decode_json
475 ! defined eval {
476 # Check that file exists
477 -f $file &&
478 # Read it
479 ($content = read_file($file)) &&
480 # Decode it
481 ($content = decode_json($content))
482 # Check expiration
483 } || (str2time($content->{expires}) <= time()+3600)
484 ) {
485 # Post new-authz request
486 my $res = $self->_post($self->{'new-authz'}, {resource => 'new-authz', identifier => {type => 'dns', value => $_}, existing => 'accept'});
487
488 # Handle error
489 unless ($res->is_success) {
490 confess 'POST '.$self->{'new-authz'}.' for '.$_.' failed: '.$res->status_line;
491 }
492
493 # Decode content
494 $content = decode_json($res->content);
495
496 # Check domain
497 unless (defined $content->{identifier}{value} && $content->{identifier}{value} eq $_) {
498 confess 'domain matching '.$content->{identifier}{value}.' for '.$_.' failed: '.$res->status_line;
499 }
500
501 # Check status
502 unless ($content->{status} eq 'valid' or $content->{status} eq 'pending') {
503 confess 'POST '.$self->{'new-authz'}.' for '.$_.' failed: '.$res->status_line;
504 }
505
506 # Write to file
507 write_file($file, encode_json($content));
508 }
509
510 # Add challenge
511 %{$self->{challenges}{$_}} = (
512 status => $content->{status},
513 expires => $content->{expires},
514 polls => []
515 );
516
517 # Save pending data
518 if ($content->{status} eq 'pending') {
519 # Extract validation data
520 foreach my $challenge (@{$content->{challenges}}) {
521 # One test already validated this auth request
522 if ($self->{challenges}{$_}{status} eq 'valid') {
523 next;
524 } elsif ($challenge->{status} eq 'valid') {
525 $self->{challenges}{$_}{status} = $challenge->{status};
526 next;
527 } elsif ($challenge->{status} eq 'pending') {
528 # Handle check
529 if (
530 ($challenge->{type} =~ /^http-01$/ and $self->_httpCheck($_, $challenge->{token})) or
531 ($challenge->{type} =~ /^dns-01$/ and $self->_dnsCheck($_, $challenge->{token}))
532 ) {
533 # Post challenge request
534 my $res = $self->_post($challenge->{uri}, {resource => 'challenge', keyAuthorization => $challenge->{token}.'.'.$self->{account}{thumbprint}});
535
536 # Handle error
537 unless ($res->is_success) {
538 confess 'POST '.$challenge->{uri}.' failed: '.$res->status_line;
539 }
540
541 # Extract content
542 my $content = decode_json($res->content);
543
544 # Save if valid
545 if ($content->{status} eq 'valid') {
546 $self->{challenges}{$_}{status} = $content->{status};
547 # Check is still polling
548 } elsif ($content->{status} eq 'pending') {
549 # Add to poll list for later use
550 push(@{$self->{challenges}{$_}{polls}}, {
551 type => (split(/-/, $challenge->{type}))[0],
552 status => $content->{status},
553 poll => $content->{uri}
554 });
555 }
556 }
557 }
558 }
559 # Check if check is challenge still in pending and no polls
560 if ($self->{challenges}{$_}{status} eq 'pending' && scalar @{$self->{challenges}{$_}{polls}} == 0) {
561 # Loop on all remaining challenges
562 foreach my $challenge (@{$content->{challenges}}) {
563 # Display help for http-01 check
564 if ($challenge->{type} eq 'http-01') {
565 print STDERR 'Create URI http://'.$_.'/.well-known/acme-challenge/'.$challenge->{token}.' with content '.$challenge->{token}.'.'.$self->{account}{thumbprint}."\n";
566 # Display help for dns-01 check
567 } elsif ($challenge->{type} eq 'dns-01') {
568 print STDERR 'Create TXT record _acme-challenge.'.$_.'. with value '.(((sha256_base64($challenge->{token}.'.'.$self->{account}{thumbprint})) =~ s/=+\z//r) =~ tr[+/][-_]r)."\n";
569 }
570 }
571 }
572 }
573 } @{$self->{domains}};
574
575 # Init max run
576 my $remaining = 10;
577
578 # Poll pending
579 while (--$remaining >= 0 and scalar map { $_->{status} eq 'valid' ? 1 : (); } values %{$self->{challenges}}) {
580 # Sleep
581 sleep(1);
582 # Poll remaining pending
583 map {
584 # Init domain
585 my $domain = $_;
586
587 # Poll remaining polls
588 map {
589 # Create a request
590 my $req = HTTP::Request->new(GET => $_->{poll});
591
592 # Get request
593 my $res = $ua->request($req);
594
595 # Handle error
596 unless ($res->is_success) {
597 carp 'GET '.$self->{challenges}{$_}{http_challenge}.' failed: '.$res->status_line if ($self->{debug});
598 }
599
600 # Extract content
601 my $content = decode_json($res->content);
602
603 # Save status
604 if ($content->{status} ne 'pending') {
605 $self->{challenges}{$domain}{status} = $content->{status};
606 }
607 } @{$self->{challenges}{$_}{polls}};
608 } map { $self->{challenges}{$_}{status} eq 'pending' ? $_ : (); } keys %{$self->{challenges}};
609 }
610
611 # Load config if available
612 my $config = undef;
613 if (
614 #XXX: use eval to workaround a fatal in decode_json
615 defined eval {
616 # Check that file exists
617 -f CONFIG &&
618 # Read it
619 ($config = read_file(CONFIG)) &&
620 # Decode it
621 ($config = decode_json($config)) &&
622 # Check defined
623 $config->{thumbprint}
624 }
625 ) {
626 # Try to write thumbprint
627 write_file($config->{thumbprint}, '');
628 }
629
630 # Stop here with remaining chanllenge
631 if (scalar map { ! defined $_->{status} or $_->{status} ne 'valid' ? 1 : (); } values %{$self->{challenges}}) {
632 # Deactivate all activated domains
633 #XXX: not implemented by letsencrypt
634 #map {
635 # # Post deactivation request
636 # my $res = $self->_post($self->{challenges}{$_}{http_uri}, {resource => 'authz', status => 'deactivated'});
637 # # Handle error
638 # unless ($res->is_success) {
639 # print Dumper($res);
640 # confess 'POST '.$self->{challenges}{$_}{http_uri}.' failed: '.$res->status_line;
641 # }
642 #} map { $self->{challenges}{$_}{status} eq 'valid' ? $_ : () } keys %{$self->{challenges}};
643
644 # Stop here as a domain of csr list failed authorization
645 if ($self->{debug}) {
646 my @domains = map { ! defined $self->{challenges}{$_}{status} or $self->{challenges}{$_}{status} ne 'valid' ? $_ : (); } keys %{$self->{challenges}};
647 confess 'Fix the challenge'.(scalar @domains > 1?'s':'').' for domain'.(scalar @domains > 1?'s':'').': '.join(', ', @domains);
648 } else {
649 exit EXIT_FAILURE;
650 }
651 }
652 }
653
654 # Issue certificate
655 sub issue {
656 my ($self) = @_;
657
658 # Open csr file
659 open(my $fh, '<', CERT_DIR.DS.REQUEST_CSR) or die $!;
660
661 # Load csr
662 my $csr = encode_base64url(join('', <$fh>) =~ s/^\0+//r);
663
664 # Close csr file
665 close($fh) or die $!;
666
667 # Post certificate request
668 my $res = $self->_post($self->{'new-cert'}, {resource => 'new-cert', csr => $csr});
669
670 # Handle error
671 unless ($res->is_success) {
672 #print Dumper($res);
673 confess 'POST '.$self->{'new-cert'}.' failed: '.$res->status_line;
674 }
675
676 # Open crt file
677 open($fh, '>', CERT_DIR.DS.SERVER_CRT) or die $!;
678
679 # Convert to pem
680 print $fh '-----BEGIN CERTIFICATE-----'."\n".encode_base64($res->content).'-----END CERTIFICATE-----'."\n";
681
682 # Create a request
683 my $req = HTTP::Request->new(GET => ACME_CERT);
684
685 # Get request
686 $res = $ua->request($req);
687
688 # Handle error
689 unless ($res->is_success) {
690 carp 'GET '.ACME_CERT.' failed: '.$res->status_line if ($self->{debug});
691 }
692
693 # Append content
694 print $fh $res->content;
695
696 # Close file
697 close($fh) or die $!;
698
699 # Print success
700 carp 'Success, pem certificate in '.CERT_DIR.DS.SERVER_CRT if ($self->{debug});
701 }
702
703 1;
704
705 __DATA__
706 #
707 # OpenSSL configuration file.
708 # This is mostly being used for generation of certificate requests.
709 #
710
711 [ req ]
712 default_bits = 2048
713 default_md = sha256
714 prompt = no
715 distinguished_name = req_distinguished_name
716 # The extentions to add to the self signed cert
717 x509_extensions = v3_ca
718 # The extensions to add to a certificate request
719 req_extensions = v3_req
720
721 # This sets a mask for permitted string types. There are several options.
722 # utf8only: only UTF8Strings (PKIX recommendation after 2004).
723 # WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings.
724 string_mask = utf8only
725
726 [ req_distinguished_name ]
727 countryName = US
728 stateOrProvinceName = State or Province Name
729 localityName = Locality Name
730 organizationName = Organization Name
731 organizationalUnitName = Organizational Unit Name
732 commonName = __COMMON_NAME__
733 emailAddress = __EMAIL_ADDRESS__
734
735 [ v3_req ]
736 basicConstraints = CA:false
737 keyUsage = nonRepudiation, digitalSignature, keyEncipherment
738 subjectAltName = email:move
739 subjectAltName = @alt_names
740
741 [ v3_ca ]
742 subjectKeyIdentifier = hash
743 authorityKeyIdentifier = keyid:always,issuer
744 basicConstraints = CA:true
745
746 [alt_names]