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

Annotation 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 - (hide annotations) (download)
Fri Mar 20 02:05:46 2015 UTC (7 years, 2 months ago) by davidg
File size: 15264 byte(s)
Initial package version

1 davidg 2774 #! /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