CA.pl.in 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. #!{- $config{HASHBANGPERL} -}
  2. # Copyright 2000-2025 The OpenSSL Project Authors. All Rights Reserved.
  3. #
  4. # Licensed under the Apache License 2.0 (the "License"). You may not use
  5. # this file except in compliance with the License. You can obtain a copy
  6. # in the file LICENSE in the source distribution or at
  7. # https://www.openssl.org/source/license.html
  8. #
  9. # Wrapper around the ca to make it easier to use
  10. #
  11. # {- join("\n# ", @autowarntext) -}
  12. use strict;
  13. use warnings;
  14. my $verbose = 1;
  15. my @OPENSSL_CMDS = ("req", "ca", "pkcs12", "x509", "verify");
  16. my $openssl = $ENV{'OPENSSL'} // "openssl";
  17. $ENV{'OPENSSL'} = $openssl;
  18. my @openssl = split_val($openssl);
  19. my $OPENSSL_CONFIG = $ENV{"OPENSSL_CONFIG"} // "";
  20. my @OPENSSL_CONFIG = split_val($OPENSSL_CONFIG);
  21. # Command invocations.
  22. my @REQ = (@openssl, "req", @OPENSSL_CONFIG);
  23. my @CA = (@openssl, "ca", @OPENSSL_CONFIG);
  24. my @VERIFY = (@openssl, "verify");
  25. my @X509 = (@openssl, "x509");
  26. my @PKCS12 = (@openssl, "pkcs12");
  27. # Default values for various configuration settings.
  28. my $CATOP = "./demoCA";
  29. my $CAKEY = "cakey.pem";
  30. my $CAREQ = "careq.pem";
  31. my $CACERT = "cacert.pem";
  32. my $CACRL = "crl.pem";
  33. my @DAYS = qw(-days 365);
  34. my @CADAYS = qw(-days 1095); # 3 years
  35. my @EXTENSIONS = qw(-extensions v3_ca);
  36. my @POLICY = qw(-policy policy_anything);
  37. my $NEWKEY = "newkey.pem";
  38. my $NEWREQ = "newreq.pem";
  39. my $NEWCERT = "newcert.pem";
  40. my $NEWP12 = "newcert.p12";
  41. # Commandline parsing
  42. my %EXTRA;
  43. my $WHAT = shift @ARGV // "";
  44. @ARGV = parse_extra(@ARGV);
  45. my $RET = 0;
  46. sub split_val {
  47. return split_val_win32(@_) if ($^O eq 'MSWin32');
  48. my ($val) = @_;
  49. my (@ret, @frag);
  50. # Skip leading whitespace
  51. $val =~ m{\A[ \t]*}ogc;
  52. # Unix shell-compatible split
  53. #
  54. # Handles backslash escapes outside quotes and
  55. # in double-quoted strings. Parameter and
  56. # command-substitution is silently ignored.
  57. # Bare newlines outside quotes and (trailing) backslashes are disallowed.
  58. while (1) {
  59. last if (pos($val) == length($val));
  60. # The first char is never a SPACE or TAB. Possible matches are:
  61. # 1. Ordinary string fragment
  62. # 2. Single-quoted string
  63. # 3. Double-quoted string
  64. # 4. Backslash escape
  65. # 5. Bare backlash or newline (rejected)
  66. #
  67. if ($val =~ m{\G([^'" \t\n\\]+)}ogc) {
  68. # Ordinary string
  69. push @frag, $1;
  70. } elsif ($val =~ m{\G'([^']*)'}ogc) {
  71. # Single-quoted string
  72. push @frag, $1;
  73. } elsif ($val =~ m{\G"}ogc) {
  74. # Double-quoted string
  75. push @frag, "";
  76. while (1) {
  77. last if ($val =~ m{\G"}ogc);
  78. if ($val =~ m{\G([^"\\]+)}ogcs) {
  79. # literals
  80. push @frag, $1;
  81. } elsif ($val =~ m{\G.(["\`\$\\])}ogc) {
  82. # backslash-escaped special
  83. push @frag, $1;
  84. } elsif ($val =~ m{\G.(.)}ogcs) {
  85. # backslashed non-special
  86. push @frag, "\\$1" unless $1 eq "\n";
  87. } else {
  88. die sprintf("Malformed quoted string: %s\n", $val);
  89. }
  90. }
  91. } elsif ($val =~ m{\G\\(.)}ogc) {
  92. # Backslash is unconditional escape outside quoted strings
  93. push @frag, $1 unless $1 eq "\n";
  94. } else {
  95. die sprintf("Bare backslash or newline in: '%s'\n", $val);
  96. }
  97. # Done if at SPACE, TAB or end, otherwise continue current fragment
  98. #
  99. next unless ($val =~ m{\G(?:[ \t]+|\z)}ogcs);
  100. push @ret, join("", splice(@frag)) if (@frag > 0);
  101. }
  102. # Handle final fragment
  103. push @ret, join("", splice(@frag)) if (@frag > 0);
  104. return @ret;
  105. }
  106. sub split_val_win32 {
  107. my ($val) = @_;
  108. my (@ret, @frag);
  109. # Skip leading whitespace
  110. $val =~ m{\A[ \t]*}ogc;
  111. # Windows-compatible split
  112. # See: "Parsing C++ command-line arguments" in:
  113. # https://learn.microsoft.com/en-us/cpp/cpp/main-function-command-line-args?view=msvc-170
  114. #
  115. # Backslashes are special only when followed by a double-quote
  116. # Pairs of double-quotes make a single double-quote.
  117. # Closing double-quotes may be omitted.
  118. while (1) {
  119. last if (pos($val) == length($val));
  120. # The first char is never a SPACE or TAB.
  121. # 1. Ordinary string fragment
  122. # 2. Double-quoted string
  123. # 3. Backslashes preceding a double-quote
  124. # 4. Literal backslashes
  125. # 5. Bare newline (rejected)
  126. #
  127. if ($val =~ m{\G([^" \t\n\\]+)}ogc) {
  128. # Ordinary string
  129. push @frag, $1;
  130. } elsif ($val =~ m{\G"}ogc) {
  131. # Double-quoted string
  132. push @frag, "";
  133. while (1) {
  134. if ($val =~ m{\G("+)}ogc) {
  135. # Two double-quotes make one literal double-quote
  136. my $l = length($1);
  137. push @frag, q{"} x int($l/2) if ($l > 1);
  138. next if ($l % 2 == 0);
  139. last;
  140. }
  141. if ($val =~ m{\G([^"\\]+)}ogc) {
  142. push @frag, $1;
  143. } elsif ($val =~ m{\G((?>[\\]+))(?=")}ogc) {
  144. # Backslashes before a double-quote are escapes
  145. my $l = length($1);
  146. push @frag, q{\\} x int($l / 2);
  147. if ($l % 2 == 1) {
  148. ++pos($val);
  149. push @frag, q{"};
  150. }
  151. } elsif ($val =~ m{\G((?:(?>[\\]+)[^"\\]+)+)}ogc) {
  152. # Backslashes not before a double-quote are not special
  153. push @frag, $1;
  154. } else {
  155. # Tolerate missing closing double-quote
  156. last;
  157. }
  158. }
  159. } elsif ($val =~ m{\G((?>[\\]+))(?=")}ogc) {
  160. my $l = length($1);
  161. push @frag, q{\\} x int($l / 2);
  162. if ($l % 2 == 1) {
  163. ++pos($val);
  164. push @frag, q{"};
  165. }
  166. } elsif ($val =~ m{\G([\\]+)}ogc) {
  167. # Backslashes not before a double-quote are not special
  168. push @frag, $1;
  169. } else {
  170. die sprintf("Bare newline in: '%s'\n", $val);
  171. }
  172. # Done if at SPACE, TAB or end, otherwise continue current fragment
  173. #
  174. next unless ($val =~ m{\G(?:[ \t]+|\z)}ogcs);
  175. push @ret, join("", splice(@frag)) if (@frag > 0);
  176. }
  177. # Handle final fragment
  178. push @ret, join("", splice(@frag)) if (@frag);
  179. return @ret;
  180. }
  181. # Split out "-extra-CMD value", and return new |@ARGV|. Fill in
  182. # |EXTRA{CMD}| with list of values.
  183. sub parse_extra
  184. {
  185. my @args;
  186. foreach ( @OPENSSL_CMDS ) {
  187. $EXTRA{$_} = [];
  188. }
  189. while (@_) {
  190. my $arg = shift(@_);
  191. if ( $arg !~ m{^-extra-(\w+)$} ) {
  192. push @args, split_val($arg);
  193. next;
  194. }
  195. $arg = $1;
  196. die "Unknown \"-extra-${arg}\" option, exiting\n"
  197. unless grep { $arg eq $_ } @OPENSSL_CMDS;
  198. die "Missing \"-extra-${arg}\" option value, exiting\n"
  199. unless (@_ > 0);
  200. push @{$EXTRA{$arg}}, split_val(shift(@_));
  201. }
  202. return @args;
  203. }
  204. # See if reason for a CRL entry is valid; exit if not.
  205. sub crl_reason_ok
  206. {
  207. my $r = shift;
  208. if ($r eq 'unspecified' || $r eq 'keyCompromise'
  209. || $r eq 'CACompromise' || $r eq 'affiliationChanged'
  210. || $r eq 'superseded' || $r eq 'cessationOfOperation'
  211. || $r eq 'certificateHold' || $r eq 'removeFromCRL') {
  212. return 1;
  213. }
  214. print STDERR "Invalid CRL reason; must be one of:\n";
  215. print STDERR " unspecified, keyCompromise, CACompromise,\n";
  216. print STDERR " affiliationChanged, superseded, cessationOfOperation\n";
  217. print STDERR " certificateHold, removeFromCRL";
  218. exit 1;
  219. }
  220. # Copy a PEM-format file; return like exit status (zero means ok)
  221. sub copy_pemfile
  222. {
  223. my ($infile, $outfile, $bound) = @_;
  224. my $found = 0;
  225. open IN, $infile || die "Cannot open $infile, $!";
  226. open OUT, ">$outfile" || die "Cannot write to $outfile, $!";
  227. while (<IN>) {
  228. $found = 1 if /^-----BEGIN.*$bound/;
  229. print OUT $_ if $found;
  230. $found = 2, last if /^-----END.*$bound/;
  231. }
  232. close IN;
  233. close OUT;
  234. return $found == 2 ? 0 : 1;
  235. }
  236. # Wrapper around system; useful for debugging. Returns just the exit status
  237. sub run
  238. {
  239. my ($cmd, @args) = @_;
  240. print "====\n$cmd @args\n" if $verbose;
  241. my $status = system {$cmd} $cmd, @args;
  242. print "==> $status\n====\n" if $verbose;
  243. return $status >> 8;
  244. }
  245. if ( $WHAT =~ /^(-\?|-h|-help)$/ ) {
  246. print STDERR <<EOF;
  247. Usage:
  248. CA.pl -newcert | -newreq | -newreq-nodes | -xsign | -sign | -signCA | -signcert | -crl | -newca [-extra-cmd parameter]
  249. CA.pl -pkcs12 [certname]
  250. CA.pl -verify certfile ...
  251. CA.pl -revoke certfile [reason]
  252. EOF
  253. exit 0;
  254. }
  255. if ($WHAT eq '-newcert' ) {
  256. # create a certificate
  257. $RET = run(@REQ, qw(-new -x509 -keyout), $NEWKEY, "-out", $NEWCERT, @DAYS, @{$EXTRA{req}});
  258. print "Cert is in $NEWCERT, private key is in $NEWKEY\n" if $RET == 0;
  259. } elsif ($WHAT eq '-precert' ) {
  260. # create a pre-certificate
  261. $RET = run(@REQ, qw(-x509 -precert -keyout), $NEWKEY, "-out", $NEWCERT, @DAYS, @{$EXTRA{req}});
  262. print "Pre-cert is in $NEWCERT, private key is in $NEWKEY\n" if $RET == 0;
  263. } elsif ($WHAT =~ /^\-newreq(\-nodes)?$/ ) {
  264. # create a certificate request
  265. $RET = run(@REQ, "-new", (defined $1 ? ($1,) : ()), "-keyout", $NEWKEY, "-out", $NEWREQ, @{$EXTRA{req}});
  266. print "Request is in $NEWREQ, private key is in $NEWKEY\n" if $RET == 0;
  267. } elsif ($WHAT eq '-newca' ) {
  268. # create the directory hierarchy
  269. my @dirs = ( "${CATOP}", "${CATOP}/certs", "${CATOP}/crl",
  270. "${CATOP}/newcerts", "${CATOP}/private" );
  271. die "${CATOP}/index.txt exists.\nRemove old sub-tree to proceed,"
  272. if -f "${CATOP}/index.txt";
  273. die "${CATOP}/serial exists.\nRemove old sub-tree to proceed,"
  274. if -f "${CATOP}/serial";
  275. foreach my $d ( @dirs ) {
  276. if ( -d $d ) {
  277. warn "Directory $d exists" if -d $d;
  278. } else {
  279. mkdir $d or die "Can't mkdir $d, $!";
  280. }
  281. }
  282. open OUT, ">${CATOP}/index.txt";
  283. close OUT;
  284. open OUT, ">${CATOP}/crlnumber";
  285. print OUT "01\n";
  286. close OUT;
  287. # ask user for existing CA certificate
  288. print "CA certificate filename (or enter to create)\n";
  289. my $FILE;
  290. $FILE = "" unless defined($FILE = <STDIN>);
  291. $FILE =~ s{\R$}{};
  292. if ($FILE ne "") {
  293. copy_pemfile($FILE,"${CATOP}/private/$CAKEY", "PRIVATE");
  294. copy_pemfile($FILE,"${CATOP}/$CACERT", "CERTIFICATE");
  295. } else {
  296. print "Making CA certificate ...\n";
  297. $RET = run(@REQ, qw(-new -keyout), "${CATOP}/private/$CAKEY",
  298. "-out", "${CATOP}/$CAREQ", @{$EXTRA{req}});
  299. $RET = run(@CA, qw(-create_serial -out), "${CATOP}/$CACERT", @CADAYS,
  300. qw(-batch -keyfile), "${CATOP}/private/$CAKEY", "-selfsign",
  301. @EXTENSIONS, "-infiles", "${CATOP}/$CAREQ", @{$EXTRA{ca}})
  302. if $RET == 0;
  303. print "CA certificate is in ${CATOP}/$CACERT\n" if $RET == 0;
  304. }
  305. } elsif ($WHAT eq '-pkcs12' ) {
  306. my $cname = $ARGV[0];
  307. $cname = "My Certificate" unless defined $cname;
  308. $RET = run(@PKCS12, "-in", $NEWCERT, "-inkey", $NEWKEY,
  309. "-certfile", "${CATOP}/$CACERT", "-out", $NEWP12,
  310. qw(-export -name), $cname, @{$EXTRA{pkcs12}});
  311. print "PKCS#12 file is in $NEWP12\n" if $RET == 0;
  312. } elsif ($WHAT eq '-xsign' ) {
  313. $RET = run(@CA, @POLICY, "-infiles", $NEWREQ, @{$EXTRA{ca}});
  314. } elsif ($WHAT eq '-sign' ) {
  315. $RET = run(@CA, @POLICY, "-out", $NEWCERT,
  316. "-infiles", $NEWREQ, @{$EXTRA{ca}});
  317. print "Signed certificate is in $NEWCERT\n" if $RET == 0;
  318. } elsif ($WHAT eq '-signCA' ) {
  319. $RET = run(@CA, @POLICY, "-out", $NEWCERT, @EXTENSIONS,
  320. "-infiles", $NEWREQ, @{$EXTRA{ca}});
  321. print "Signed CA certificate is in $NEWCERT\n" if $RET == 0;
  322. } elsif ($WHAT eq '-signcert' ) {
  323. $RET = run(@X509, qw(-x509toreq -in), $NEWREQ, "-signkey", $NEWREQ,
  324. qw(-out tmp.pem), @{$EXTRA{x509}});
  325. $RET = run(@CA, @POLICY, "-out", $NEWCERT,
  326. qw(-infiles tmp.pem), @{$EXTRA{ca}}) if $RET == 0;
  327. print "Signed certificate is in $NEWCERT\n" if $RET == 0;
  328. } elsif ($WHAT eq '-verify' ) {
  329. my @files = @ARGV ? @ARGV : ( $NEWCERT );
  330. foreach my $file (@files) {
  331. my $status = run(@VERIFY, "-CAfile", "${CATOP}/$CACERT", $file, @{$EXTRA{verify}});
  332. $RET = $status if $status != 0;
  333. }
  334. } elsif ($WHAT eq '-crl' ) {
  335. $RET = run(@CA, qw(-gencrl -out), "${CATOP}/crl/$CACRL", @{$EXTRA{ca}});
  336. print "Generated CRL is in ${CATOP}/crl/$CACRL\n" if $RET == 0;
  337. } elsif ($WHAT eq '-revoke' ) {
  338. my $cname = $ARGV[0];
  339. if (!defined $cname) {
  340. print "Certificate filename is required; reason optional.\n";
  341. exit 1;
  342. }
  343. my @reason;
  344. @reason = ("-crl_reason", $ARGV[1])
  345. if defined $ARGV[1] && crl_reason_ok($ARGV[1]);
  346. $RET = run(@CA, "-revoke", $cname, @reason, @{$EXTRA{ca}});
  347. } else {
  348. print STDERR "Unknown arg \"$WHAT\"\n";
  349. print STDERR "Use -help for help.\n";
  350. exit 1;
  351. }
  352. exit $RET;