1 #! /usr/bin/perl |
|
2 use strict; |
|
3 use warnings; |
|
4 use Template; |
|
5 use IO::File; |
|
6 use File::Path; |
|
7 use File::Temp qw(tempdir); |
|
8 use File::Basename; |
|
9 use Getopt::Long qw(GetOptionsFromArray); |
|
10 use Pod::Usage; |
|
11 use feature qw(switch); |
|
12 |
|
13 my $CA_CRT = "CA/ca-crt.pem"; |
|
14 my $CA_KEY = "CA/private/ca-key.pem"; |
|
15 my $CA_DIR = "./var"; |
|
16 |
|
17 my %TEMPLATE = ( |
|
18 ca => "lib/templates/ca", |
|
19 req => "lib/templates/req", |
|
20 ); |
|
21 |
|
22 my $TMP = tempdir("/tmp/$ENV{USER}.ca.XXXXXX", CLEANUP => 1); |
|
23 |
|
24 my $opt_days = undef; # see the templates/ca for a default |
|
25 my $opt_type = undef; # see the templates/ca for a default |
|
26 my $opt_policy = "de"; # see the templates/ca for a default |
|
27 my $opt_outfile = undef; |
|
28 my $opt_force = undef; |
|
29 |
|
30 sub init_ca(); |
|
31 sub ask_pass($); |
|
32 |
|
33 MAIN: { |
|
34 my $csrfile; |
|
35 |
|
36 GetOptions( |
|
37 "d|days=i" => \$opt_days, |
|
38 "t|type=s" => \$opt_type, |
|
39 "p|policy=s" => \$opt_policy, |
|
40 "o|outfile=s" => \$opt_outfile, |
|
41 "f|force" => \$opt_force, |
|
42 "i|init" => sub { eval { init_ca() }; if ($@) { warn $@; exit 1 }; exit 0 }, |
|
43 "h|help" => sub { pod2usage(-verbose => 1, -exit => 0) }, |
|
44 "m|man" => sub { pod2usage(-verbose => 2, -exit => 0) }, |
|
45 ) or pod2usage; |
|
46 |
|
47 pod2usage if @ARGV > 1; |
|
48 $csrfile = $ARGV[0]; # don't shift, we'll need it later! |
|
49 |
|
50 my $cnf = new IO::File ">$TMP/cnf" or die "Can't open >$TMP/cnf: $!\n"; |
|
51 my $csr = new IO::File "+>$TMP/csr" or die "Can't open +>$TMP/csr: $!\n"; |
|
52 my $crt = new IO::File "+>$TMP/crt" or die "Can't open +>$TMP/crt: $!\n"; |
|
53 my $tt2 = new Template or die $Template::ERROR; |
|
54 |
|
55 # get a private copy of the request |
|
56 print { IO::File->new("|openssl req -out $TMP/csr") } <>; |
|
57 open(STDIN, "</dev/tty") if not defined $csrfile; |
|
58 |
|
59 die "CSR is empty" if not -s $csr; |
|
60 |
|
61 $tt2->process( |
|
62 $TEMPLATE{ca}, |
|
63 { |
|
64 type => $opt_type, |
|
65 days => $opt_days, |
|
66 policy => "policy_$opt_policy", |
|
67 cacrt => $CA_CRT, |
|
68 cakey => $CA_KEY, |
|
69 cadir => $CA_DIR, |
|
70 } => "$TMP/cnf" |
|
71 ) or die $tt2->error, "\n"; |
|
72 |
|
73 system( "openssl ca -config $TMP/cnf -in $TMP/csr -out $TMP/crt" |
|
74 . " -utf8 \${CA_PASS:+-passin env:CA_PASS}"); |
|
75 |
|
76 die "ERR: Cert is zero size\n" if not -s $crt; |
|
77 |
|
78 # get the name of the output crt file |
|
79 my $outfile = $opt_outfile; |
|
80 if (not defined $outfile and defined($_ = $csrfile)) { |
|
81 if (/(.*[\W_])(?:req|csr).pem$/) { $outfile = "$1crt.pem" } |
|
82 elsif (/(.*[\W_])req$/) { $outfile = "$1crt" } |
|
83 else { $outfile .= ".crt.pem" } |
|
84 } |
|
85 |
|
86 # to be sure not to have an invalid/dangerous file name |
|
87 if (fork() == 0) { |
|
88 if (defined $outfile) { |
|
89 open(STDOUT, ">$outfile") |
|
90 or die "Can't open >$outfile: $!\n"; |
|
91 } |
|
92 exec "openssl x509 -in $TMP/crt"; |
|
93 die "Can't exec openssl x509: $!\n"; |
|
94 } |
|
95 else { wait } |
|
96 |
|
97 # and now, since it's finally done, we'll copy the request |
|
98 # away (for later use (thing about re-issuing a certificate)) |
|
99 my $subject = `openssl x509 -in $TMP/crt -noout -subject`; |
|
100 if (my ($cn) = $subject =~ /CN=(\S+?)[,\/\s\$]/) { |
|
101 if (fork() == 0) { |
|
102 open(STDOUT, ">$CA_DIR/requests/$cn-csr.pem") |
|
103 or die "Can't open >$CA_DIR/requests/$cn-csr.pem: $!\n"; |
|
104 exec "openssl req -in $TMP/csr"; |
|
105 die "Can't exec openssl req: $!\n"; |
|
106 } |
|
107 else { wait } |
|
108 } |
|
109 else { |
|
110 die "Can't determine the CN from $subject, not saving the request\n"; |
|
111 } |
|
112 |
|
113 exit; |
|
114 } |
|
115 |
|
116 sub ask_pass($) { |
|
117 my $prompt = shift; |
|
118 my @keys = ("x", "y"); |
|
119 |
|
120 while (1) { |
|
121 print $prompt; |
|
122 my $stty = `stty -g`; |
|
123 system("stty -echo"); |
|
124 chomp($keys[0] = IO::File->new("/dev/tty")->getline()); |
|
125 print "\n"; |
|
126 system("stty $stty"); |
|
127 print "please again for verification: "; |
|
128 system("stty -echo"); |
|
129 chomp($keys[1] = IO::File->new("/dev/tty")->getline()); |
|
130 print "\n"; |
|
131 system("stty $stty"); |
|
132 return $keys[0] if $keys[0] eq $keys[1]; |
|
133 print "keys mismatch, again\n"; |
|
134 } |
|
135 } |
|
136 |
|
137 sub init_ca() { |
|
138 |
|
139 # initialize the CA directory structure. This should |
|
140 # correspond to the values found in templates/ca |
|
141 die "$CA_DIR already exists" if -d $CA_DIR and not $opt_force; |
|
142 mkpath(map { "$CA_DIR/$_" } qw(newcerts requests)); |
|
143 mkpath(map { dirname $_ } $CA_CRT, $CA_KEY); |
|
144 (new IO::File ">$CA_DIR/index"); |
|
145 (new IO::File ">$CA_DIR/serial")->print("01\n"); |
|
146 |
|
147 # now |
|
148 my $tt2 = new Template or die $Template::ERROR; |
|
149 $tt2->process( |
|
150 $TEMPLATE{req}, |
|
151 { |
|
152 |
|
153 # not used yet |
|
154 } => "$TMP/cnf" |
|
155 ) or die $tt2->error; |
|
156 |
|
157 $ENV{CA_PASS} = ask_pass("passphrase for CA key: "); |
|
158 system( |
|
159 "openssl req -config $TMP/cnf -x509 -days 3650 -new -passout env:CA_PASS -keyout $TMP/ca-key.pem -out $TMP/ca-crt.pem" |
|
160 ) and exit; |
|
161 |
|
162 system("openssl x509 -in $TMP/ca-crt.pem -out $CA_CRT") and exit; |
|
163 $_ = umask(077); |
|
164 system( |
|
165 "openssl rsa -in $TMP/ca-key.pem -des3 -passin env:CA_PASS -passout env:CA_PASS -out $CA_KEY" |
|
166 ) and exit; |
|
167 umask($_); |
|
168 |
|
169 return 0; |
|
170 |
|
171 } |
|
172 |
|
173 __END__ |
|
174 |
|
175 =head1 NAME |
|
176 |
|
177 ca - the ultimative CA tool |
|
178 |
|
179 =head1 SYNOPSIS |
|
180 |
|
181 ca [--force] --init |
|
182 ca --type=TYPE --days=DAYS [request.pem] |
|
183 |
|
184 (not yet: request c=COUNTRY ST=STATE l=LOCATION o=ORGANIZATION OU=ORG-UNIT cn=COMMON-NAME) |
|
185 |
|
186 =head1 DESCRIPTION |
|
187 |
|
188 This B<ca> tool signs the request file. If no file is given, it |
|
189 expects the request on STDIN |
|
190 |
|
191 =head1 OPTIONS |
|
192 |
|
193 =over 4 |
|
194 |
|
195 =item B<-d>|B<--days> I<days> |
|
196 |
|
197 The number of days the certificate should be valid. (default: 365) |
|
198 |
|
199 =item B<-h>|B<--help> |
|
200 |
|
201 Print the reference help and exit. (default: off) |
|
202 |
|
203 =item B<-i>|B<--init> |
|
204 |
|
205 Initialize the CA (keys, directories). This may be enforce with |
|
206 B<--force>. (default: off) |
|
207 |
|
208 =item B<-m>|B<--man> |
|
209 |
|
210 Open the reference manual and exit. (default: off) |
|
211 |
|
212 =item B<-o>|B<--out> I<outfile> |
|
213 |
|
214 The name of the output file. If not set (the default), the output goes |
|
215 to I<stdout> if the CSR came from stdin and it goes to a file named |
|
216 similar to the CSR, if the request came from a file. |
|
217 |
|
218 =item B<-t>|B<--type> I<type> |
|
219 |
|
220 The (NSCertType) type of the certificate. Should be client or server. |
|
221 (default: none) |
|
222 |
|
223 =back |
|
224 |
|
225 =cut |
|
226 ## Please see file perltidy.ERR |
|