/[pdpsoft]/nl.nikhef.pdp.tcs/nl.nikhef.pdp.tcs.dctcs-cli/trunk/dctcs-cli.cin
ViewVC logotype

Contents of /nl.nikhef.pdp.tcs/nl.nikhef.pdp.tcs.dctcs-cli/trunk/dctcs-cli.cin

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2774 - (show annotations) (download)
Fri Mar 20 02:05:46 2015 UTC (6 years, 10 months ago) by davidg
File size: 15264 byte(s)
Initial package version

1 #! /usr/bin/perl -w
2 #
3 # dctcs-cli.pl - DigiCert TCS server certificate generator CLI
4 # see below at end for documentation and arguments
5 #
6 # @(#)$Id$
7 # David Groep, Nikhef, 2015 - www.nikhef.nl/grid
8 #
9 # As per doc https://www.digicert.com/services/v2/documentation
10 #
11 use strict;
12 use LWP::UserAgent;
13 use LWP::Protocol::https;
14 use IO::Socket::SSL;
15 use JSON;
16 use Data::Dumper;
17 use Getopt::Long;
18 $Getopt::Long::ignorecase = 0;
19
20 # ###########################################################################
21 # basic configuration - you SHOULD probably change apikeyfile and orgid!
22 # where apikeyfile may be the empty string (will then ask key from STDIN)
23 #
24 my $resturl = "https://www.digicert.com/services/v2";
25 my $thishost = `hostname -f`; chomp($thishost);
26 my $dirprefix = "tcs-";
27 my $apikeylen = 82; # length in characters of API key, seems to be 47 or 82
28 #
29 # CONFIGURE these values or override with args each time
30 my $orgid = "@ORGNAME@"; # CONFIG: Org ID from DigiCert, see also GUI
31 my $apikeyfile = "@APIKEYFILE@"; # CONFIG: provide your own filename here
32
33 my $hostname;
34 my $dir; # target directory (default to tcs-<hostname>)
35 my $subdir=""; # target subdirecty (defaults to empty)
36 my $userelease=0; # use NDPF vlaai release mechanism
37 my $mode_req=0;
38 my $mode_retr=0;
39 my $mode_appr=0;
40 my $basedir="";
41 my $product = "grid_host_ssl_multi_domain";
42 my $validity = 1;
43 my $approve = "";
44 my $help = 0;
45
46 &GetOptions(
47 'd|destdir=s' => \$dir,
48 's|subsir=s' => \$subdir,
49 'R|release' => \$userelease,
50 'K|keyfile=s' => \$apikeyfile,
51 'prefix=s' => \$dirprefix,
52 'P|product=s' => \$product,
53 'A|approve=s' => \$approve,
54 'O|orgid=s' => \$orgid,
55 'V|val|validity=i' => \$validity,
56 'r|request' => \$mode_req,
57 'a|postapprove' => \$mode_appr,
58 'i|install|retrieve' => \$mode_retr,
59 'h|help' => \$help
60 ) or exit 1;
61
62 if ( $help ) { &help; exit 0 }
63
64 # ###########################################################################
65 #
66 # validate options and input
67 die "Requires a mode (-r or -i or -a) for running\n"
68 if ! $mode_req and ! $mode_retr and ! $mode_appr;
69
70 $hostname = $ARGV[0] or die "No argument hostname";
71 $hostname =~ s/^$dirprefix//; # if you used the dirname instead ...
72 die "Invalid hostname $hostname\n"
73 if ( $hostname !~ /^[a-z][-0-9a-z]+\.[a-z][-0-9a-z\.]+$/ );
74
75 $dir = "$dirprefix$hostname" unless $dir;
76 $basedir=$dir;
77 if ( $subdir ne "" ) { $dir .= "/$subdir"; }
78
79 die "Invalid subdir (not a SUBdir) in $subdir\n" if $subdir =~ /^\//;
80 die "Cannot retrieve cert for $hostname if not requested this way,\n".
81 "and directory $dir does not exist\n" if ( $mode_retr and ! -d $dir );
82 die "Cannot re-use existing $dir for request\n"
83 if $mode_req and -d $dir;
84 die "TCS directory $dir has no orderID\n"
85 if $mode_retr and ! -e "$dir/orderid.txt";
86 die "Invalid validity (not a number or too long): $validity\n"
87 if $validity !~ /^\d+$/ or $validity > 3;
88 die "Cannot approve on installation (but on request or explicit only)\n"
89 if $mode_retr && $approve;
90 die "Cannot approve without an approval comment\n"
91 if $mode_appr && ( $approve eq "" );
92
93 die "Expected argument FQDN\n" if ( $#ARGV < 0 );
94
95 # ###########################################################################
96 #
97 # read password if needed from file or use env var DIGICERTAPIKEY or STDIN
98 my $apikey;
99 if ( $apikeyfile ne "none" && $apikeyfile ne "" && -e $apikeyfile ) {
100 open FH,"<$apikeyfile";
101 $apikey = <FH>; chomp($apikey);
102 close FH;
103 } elsif (defined($ENV{DIGICERTAPIKEY})) {
104 $apikey = $ENV{DIGICERTAPIKEY};
105 } else {
106 print "Provide API key: ";
107 system("stty -echo");
108 $apikey = <STDIN>; chomp($apikey);
109 system("stty echo"); print "***\n";
110 }
111 #die "Invalid API key length\n" if length($apikey) != $apikeylen;
112
113 # ###########################################################################
114 # setup defaults and LWP
115 #
116 # initialise UA
117 my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => 1 });
118 $ua->agent("dctcs/0.2 (libwww-perl/$]; TERENA-TCS; $^O)");
119 $ua->default_header('X-DC-DEVKEY' => $apikey);
120 $ua->default_header('Content-Type' => "application/json");
121 $ua->default_header('Accept' => "application/json");
122
123 # ###########################################################################
124 # Actions: request or install
125 #
126 if ( $mode_req ) {
127 # #########################################################################
128 #
129 print "Creating request and order for $hostname\n";
130
131 if ( ! -d $basedir && $subdir ne "" ) {
132 mkdir $basedir or
133 die "Cannot create new basedir $basedir for $hostname: $!\n";
134 }
135 mkdir $dir or die "Cannot create new subdir $dir for $hostname: $!\n";
136
137 # generate request using OpenSSL
138 my $rc = system("openssl req -new -nodes -keyout $dir/key-$hostname.pem ".
139 "-out $dir/req-$hostname.pem -newkey rsa:2048 ".
140 "-subj '/C=NL/O=Nikhef/CN=$hostname'");
141 die "Generation of CSR in $dir failed: $rc\n" if $rc;
142
143 # construct request for DigiCert API v2
144 open CSR,"<$dir/req-$hostname.pem" or die "Cannot open CSR: $!\n";
145 my $csr = ""; while (<CSR>) { $csr .= $_; }; close CSR;
146
147 # if the org is named, we need to resolve it to an ID in the
148 # user's current container
149 if ( $orgid !~ /^\d+$/ ) { # it is a named org, so resolve
150 my $containerid = &getContainerId($ua);
151 print "Resolving org $orgid in container $containerid\n";
152
153 my $oanswer = &getDump($ua,"GET",
154 "container/$containerid/order/organization" );
155
156 foreach my $iorg ( @{$oanswer->{"organizations"}} ) {
157 if ( lc($iorg->{"name"}) eq lc($orgid) ) {
158 $orgid = $iorg->{"id"}; last;
159 }
160 }
161 die "Cannot resolve org $orgid via API\n" if ( $orgid !~ /^\d+$/ );
162 print "Processing order for org #$orgid in container #$containerid\n";
163 }
164
165 my %request = (
166 "certificate" => {
167 "common_name", "$hostname",
168 "dns_names" , [ ],
169 "csr", "$csr",
170 "signature_hash", "sha256"
171 },
172 "organization" => { "id", "$orgid" },
173 "validity_years", $validity,
174 "comments", "Requested on $thishost for NDPF host $hostname ".
175 "by ".getpwuid( $< ),
176 );
177
178 # populate subjectAltName, with at least the primary FQDN
179 $request{"certificate"}{"dns_names"} = [];
180 foreach my $hn ( @ARGV ) {
181 push @{$request{"certificate"}{"dns_names"}},$hn;
182 }
183
184 my $answer = &getDump($ua,"POST",
185 "order/certificate/$product",to_json(\%request) );
186
187 # store the OrderID, used by the installed, in a special file
188 if ( defined $answer->{"id"} ) {
189 open FH,">$dir/orderid.txt" and do {
190 print FH $answer->{"id"}."\n";
191 close FH;
192 };
193 };
194
195 # save the full JSON answer, including requestID, for later perusal
196 open FH,">$dir/response.json" and do {
197 print FH to_json($answer);
198 close FH;
199 };
200
201 # approval: needs a comment, and can only be done by Admins of CertCentral
202 if ( $approve ne "" ) {
203 my $requestid = $answer->{"requests"}[0]{"id"};
204 if ( $requestid =~ /^\d+$/ ) {
205 print "Approving request (attempt) for Request ID $requestid\n";
206 my %approval = (
207 "status" => "approved",
208 "processor_comment" => "By ".getpwuid( $< ).": $approve"
209 );
210 my $approvalresult = &getDump($ua,"PUTDUMP","request/$requestid/status",
211 to_json(\%approval));
212 print "Approved: $approvalresult\n";
213 } else {
214 warn "Invalid request ID found ($requestid), sorry!\n";
215 }
216 }
217 #
218 # #########################################################################
219
220 } elsif ( $mode_retr ) {
221
222 # #########################################################################
223 #
224
225 # works based on stored order number in dirctory
226 print "Retrieving order for $hostname\n";
227
228 open ORDERID,"<$dir/orderid.txt" or die "Cannot open OrderID $!\n";
229 my $orderid = <ORDERID>; chomp($orderid);
230 die "Invalid order ID $orderid\n" unless $orderid =~ /^\d+$/;
231
232 my $answer = &getDump($ua,"GET","order/certificate/$orderid");
233
234 die "No certificate for order $orderid (yet)\n"
235 unless defined $answer->{"certificate"}{"id"} and
236 $answer->{"certificate"}{"id"} =~ /^\d+$/;
237 my $certid = $answer->{"certificate"}{"id"};
238
239 print "Retrieving order $orderid certificate ID $certid\n";
240
241 my $certdata =
242 &getDump($ua,"GETDUMP","/certificate/$certid/download/format/pem_all");
243
244 # this splits the full PEM file of all into per-cert chunks with good naming
245 my $i = 0; my $str = ""; my $processing = 0; my $eecname=""; my $l;
246
247 my @certlist = split /[\r\n]+/, $certdata; # irrespective of EOLN in input
248 foreach $l ( @certlist ) {
249 if ( $l eq "-----BEGIN CERTIFICATE-----" ) {
250 $str = ""; $processing = 1;
251 }
252 if ( $l eq "-----END CERTIFICATE-----" ) {
253 my ($subj,$olstr,$cn,$fname);
254
255 $str .= "$l\n"; $processing = 0;
256 ( $olstr = $str ) =~ s/\n/\\n/gm;
257
258 $subj = `echo -ne "$olstr" | openssl x509 -noout -subject`;
259 chomp($subj); $subj =~ s/^[^=]+= *//;
260
261 ( $cn = $subj ) =~ s/^.*\/CN=//;
262 ( $fname = $cn ) =~ s/\s+//g;
263 open CF,">$dir/cert-$fname.pem" and do {
264 print CF $str;
265 close CF;
266 if ( $i == 0 and $cn eq $hostname ) {
267 $eecname = "$dir/cert-$fname.pem";
268 }
269 };
270 # informational output follows
271 print "Certificate $i (cert-$cn):\n";
272 print " Subject: $subj\n";
273 $i++;
274 }
275 if ( $processing ) { $str .= "$l\n"; }
276 }
277
278 # cater for subdir and release to usercert/userkey.pem
279 if ( $userelease and $eecname ) {
280 if ( $eecname ne "$dir/cert-$hostname.pem") {
281 die "Naming inconsistency for $eecname, oops! ".
282 "It is not $basedir/$subdir/cert-$hostname.pem\n";
283 }
284 if ( ( -e "$basedir/usercert.pem" && ! -l "$basedir/usercert.pem" )
285 || ( -e "$basedir/userkey.pem" && ! -l "$basedir/userkey.pem" ) ) {
286 warn "One of $basedir/user{key,cert}.pem not a symlink, release skipped"
287 } else {
288 unlink "$basedir/usercert,pem";
289 unlink "$basedir/userkey,pem";
290 $subdir = "." unless $subdir;
291 symlink "$subdir/key-$hostname.pem","$basedir/userkey.pem";
292 symlink "$subdir/cert-$hostname.pem","$basedir/usercert.pem";
293 open FH,">$basedir/release.state" and close FH;
294 }
295 }
296
297 #
298 # #########################################################################
299
300 } elsif ( $mode_appr ) {
301
302 # #########################################################################
303 #
304
305 # works based on stored order number in dirctory
306 print "Approving first request for order for $hostname\n";
307
308 open ORDERID,"<$dir/orderid.txt" or die "Cannot open OrderID $!\n";
309 my $orderid = <ORDERID>; chomp($orderid);
310 die "Invalid order ID $orderid\n" unless $orderid =~ /^\d+$/;
311
312 my $answer = &getDump($ua,"GET","order/certificate/$orderid");
313
314 die "No request for order $orderid (yet)\n"
315 unless defined $answer->{"requests"}[0]{"id"} and
316 $answer->{"requests"}[0]{"id"} =~ /^\d+$/;
317 my $requestid = $answer->{"requests"}[0]{"id"};
318
319 if ( $answer->{"status"} ne "needs_approval" ) {
320 print "Order $orderid (request $requestid) already approved, skipping\n";
321 exit 0;
322 }
323
324 print "Retrieving order $orderid request ID $requestid\n";
325
326 # approval: needs a comment, and can only be done by Admins of CertCentral
327 print "Approving request (attempt) for Request ID $requestid\n";
328 my %approval = (
329 "status" => "approved",
330 "processor_comment" => "By ".getpwuid( $< ).": $approve"
331 );
332 my $approvalresult = &getDump($ua,"PUTDUMP","request/$requestid/status",
333 to_json(\%approval));
334 print "Approved: $approvalresult\n";
335
336 # save the full JSON answer, including requestID, for later perusal
337 open FH,">$dir/response-approval.json" and do {
338 print FH $approvalresult;
339 close FH;
340 };
341 }
342
343 exit 0;
344
345 # ###########################################################################
346 #
347
348 # getDump($ua,"(GET|PUT|GETDUMP|PUTDUMP|POST|POSTDUMP)",$url,[$content])
349 # where the "DUMP" modes will return plain text from the answer, but
350 # the default modes will return a perl object created from the JSON
351 #
352 sub getDump($$$$) {
353 my ($ua,$type,$request,$content) = @_;
354 my $data;
355 $type = "GET" unless (defined $type and $type ne "");
356 die "Invalid call with GET and contents\n"
357 if ( $type eq "GET" and defined $content and $content ne "");
358
359 my $outtype = $type;
360 $type =~ s/DUMP$//;
361
362 my $req = HTTP::Request->new($type => "$resturl/$request");
363 if ( ( $type eq "POST" || $type eq "PUT" )
364 and defined $content and $content ne "" ) {
365 $req->content($content);
366 }
367
368 my $res = $ua->request($req);
369
370 if ($res->is_success) {
371 if ( $outtype =~ /DUMP/ ) { $data = $res->content; }
372 else { $data = from_json($res->content); }
373 } else {
374 die "Invalid API call: ", $res->status_line, "\n";
375 }
376 return $data;
377 }
378
379 # example of a specific API wrapper
380 sub getContainerId($) {
381 my ($ua) = @_;
382 my $data;
383
384 my $req = HTTP::Request->new(GET => "$resturl/user/me");
385 my $res = $ua->request($req);
386
387 if ($res->is_success) {
388 $data = from_json($res->content);
389 } else {
390 die "Invalid API call: ", $res->status_line, "\n";
391 }
392 return $data->{container}{id};
393 }
394
395 # ###########################################################################
396 # Valid products
397 #
398 # 'name_id' => 'client_digital_signature_plus',
399 # 'name_id' => 'client_email_security_plus',
400 # 'name_id' => 'client_grid_premium',
401 # 'name_id' => 'client_grid_robot_email',
402 # 'name_id' => 'client_grid_robot_fqdn',
403 # 'name_id' => 'client_grid_robot_name',
404 # 'name_id' => 'client_premium',
405 # 'name_id' => 'code_signing',
406 # 'name_id' => 'code_signing_ev',
407 # 'name_id' => 'document_signing_org_1',
408 # 'name_id' => 'document_signing_org_2',
409 # 'name_id' => 'grid_host_ssl',
410 # 'name_id' => 'grid_host_ssl_multi_domain',
411 # 'name_id' => 'ssl_ev_multi_domain',
412 # 'name_id' => 'ssl_ev_plus',
413 # 'name_id' => 'ssl_multi_domain',
414 # 'name_id' => 'ssl_plus',
415 # 'name_id' => 'ssl_wildcard',
416 #
417
418 # ###########################################################################
419 # HELP
420
421 sub help() {
422
423 ( my $base = $0 ) =~ s/^.*\///;
424 print <<EOF;
425 Request and retrieve certificates from the TCS DigiCert service via the API
426
427 $base [-P product] [-R] [-s path] [-d basedir] [-A comment] [-K keyfile]
428 [-O orgid]
429 [-r|-i] hostname [altname ...]
430
431 -r enter REQUEST mode + either -r or -i required
432 -i enter INSTALLATION mode +
433
434 -P <product> order <product>, with "grid_host_ssl_multi_domain" the default
435 but "ssl_multi_domain" also useful. See below
436 -V validity validity request period in years (default: 1)
437 -K keyfile file with the API key for the user as a single line
438 -O orgid Organisation name or ID
439 -s subdir use <subdir> for key, cert, and orderid storage (no default)
440 -R use the NDPF vlaai symlink & release.state mechanism
441 which works best with subdir usage (will touch release.state)
442 -A comment Approve request as well, with "comment" (admins only)
443 --prefix=dir dir prefix (defaults to "tcs-")
444
445 All certs are requested with SHA256 digest. Other products that might
446 work with this script are:
447 grid_host_ssl, grid_host_ssl_multi_domain
448 ssl_ev_multi_domain, ssl_ev_plus
449 ssl_multi_domain, ssl_plus
450 But note that EV requires an extra approval step by the EV admin, and
451 that wildcard certs will mess up the directory naming.
452 EOF
453 return 0;
454 }

Properties

Name Value
svn:executable *

grid.support@nikhef.nl
ViewVC Help
Powered by ViewVC 1.1.28