ovs-pki: Consistently write error messages to stderr.
[sliver-openvswitch.git] / utilities / ovs-pki.in
1 #! /bin/sh
2
3 # Copyright (c) 2008, 2009, 2010 Nicira Networks, Inc.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at:
8 #
9 #     http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 set -e
18
19 pkidir='@PKIDIR@'
20 command=
21 prev=
22 force=no
23 batch=no
24 log='@LOGDIR@/ovs-pki.log'
25 keytype=rsa
26 bits=2048
27 for option; do
28     # This option-parsing mechanism borrowed from a Autoconf-generated
29     # configure script under the following license:
30
31     # Copyright (C) 1992, 1993, 1994, 1995, 1996, 1998, 1999, 2000, 2001,
32     # 2002, 2003, 2004, 2005, 2006, 2009 Free Software Foundation, Inc.
33     # This configure script is free software; the Free Software Foundation
34     # gives unlimited permission to copy, distribute and modify it.
35
36     # If the previous option needs an argument, assign it.
37     if test -n "$prev"; then
38         eval $prev=\$option
39         prev=
40         continue
41     fi
42     case $option in
43         *=*) optarg=`expr "X$option" : '[^=]*=\(.*\)'` ;;
44         *) optarg=yes ;;
45     esac
46
47     case $dashdash$option in
48         --)
49             dashdash=yes ;;
50         -h|--help)
51             cat <<EOF
52 ovs-pki, for managing a simple OpenFlow public key infrastructure 
53 usage: $0 [OPTION...] COMMAND [ARG...]
54
55 The valid stand-alone commands and their arguments are:
56   init                 Initialize the PKI
57   req NAME             Create new private key and certificate request
58                        named NAME-privkey.pem and NAME-req.pem, resp.
59   sign NAME [TYPE]     Sign switch certificate request NAME-req.pem,
60                        producing certificate NAME-cert.pem
61   req+sign NAME [TYPE] Combine the above two steps, producing all three files.
62   verify NAME [TYPE]   Checks that NAME-cert.pem is a valid TYPE certificate
63   fingerprint FILE     Prints the fingerprint for FILE
64   self-sign NAME       Sign NAME-req.pem with NAME-privkey.pem,
65                        producing self-signed certificate NAME-cert.pem
66
67 The following additional commands manage an online PKI:
68   ls [PREFIX] [TYPE]   Lists incoming requests of the given TYPE, optionally 
69                        limited to those whose fingerprint begins with PREFIX
70   flush [TYPE]         Rejects all incoming requests of the given TYPE
71   reject PREFIX [TYPE] Rejects the incoming request(s) whose fingerprint begins
72                        with PREFIX and has the given TYPE
73   approve PREFIX [TYPE] Approves the incoming request whose fingerprint begins
74                        with PREFIX and has the given TYPE
75   expire [AGE]         Rejects all incoming requests older than AGE, in
76                        one of the forms Ns, Nmin, Nh, Nday (default: 1day)
77   prompt [TYPE]        Interactively prompts to accept or reject each incoming
78                        request of the given TYPE
79
80 Each TYPE above is a certificate type: 'switch' (default) or 'controller'.
81
82 Options for 'init', 'req', and 'req+sign' only:
83   -k, --key=rsa|dsa    Type of keys to use (default: rsa)
84   -B, --bits=NBITS     Number of bits in keys (default: 2048).  For DSA keys,
85                          this has an effect only on 'init'.
86   -D, --dsaparam=FILE  File with DSA parameters (DSA only)
87                          (default: dsaparam.pem within PKI directory)
88 Options for use with the 'sign' and 'approve' commands:
89   -b, --batch          Skip fingerprint verification
90 Options that apply to any command:
91   -d, --dir=DIR        Directory where the PKI is located
92                          (default: $pkidir)
93   -f, --force          Continue even if file or directory already exists
94   -l, --log=FILE       Log openssl output to FILE (default: ovs-log.log)
95   -h, --help           Print this usage message.
96 EOF
97             exit 0
98             ;;
99         --di*=*)
100             pkidir=$optarg
101             ;;
102         --di*|-d)
103             prev=pkidir
104             ;;
105         --k*=*)
106             keytype=$optarg
107             ;;
108         --k*|-k)
109             prev=keytype
110             ;;
111         --bi*=*)
112             bits=$optarg
113             ;;
114         --bi*|-B)
115             prev=bits
116             ;;
117         --ds*=*)
118             dsaparam=$optarg
119             ;;
120         --ds*|-D)
121             prev=dsaparam
122             ;;
123         --l*=*)
124             log=$optarg
125             ;;
126         --l*|-l)
127             prev=log
128             ;;
129         --force|-f)
130             force=yes
131             ;;
132         --ba*|-b)
133             batch=yes
134             ;;
135         -*)
136             echo "unrecognized option $option" >&2
137             exit 1
138             ;;
139         *)
140             if test -z "$command"; then
141                 command=$option
142             elif test -z "${arg1+set}"; then
143                 arg1=$option
144             elif test -z "${arg2+set}"; then
145                 arg2=$option
146             else
147                 echo "$option: only two arguments may be specified" >&2
148                 exit 1
149             fi
150             ;;
151     esac
152     shift
153 done
154 if test -n "$prev"; then
155     option=--`echo $prev | sed 's/_/-/g'`
156     { echo "$as_me: error: missing argument to $option" >&2
157         { (exit 1); exit 1; }; }
158 fi
159 if test -z "$command"; then
160     echo "$0: missing command name; use --help for help" >&2
161     exit 1
162 fi
163 if test "$keytype" != rsa && test "$keytype" != dsa; then
164     echo "$0: argument to -k or --key must be rsa or dsa" >&2
165     exit 1
166 fi
167 if test "$bits" -lt 1024; then
168     echo "$0: argument to -B or --bits must be at least 1024" >&2
169     exit 1
170 fi
171 if test -z "$dsaparam"; then
172     dsaparam=$pkidir/dsaparam.pem
173 fi
174 case $log in
175     /*) ;;
176     *) $log="$PWD/$log" ;;
177 esac
178
179 if test "$command" = "init"; then
180     if test -e "$pkidir" && test "$force" != "yes"; then
181         echo "$0: $pkidir already exists and --force not specified" >&2
182         exit 1
183     fi
184
185     if test ! -d "$pkidir"; then
186         mkdir -p "$pkidir"
187     fi
188     cd "$pkidir"
189     exec 3>>$log
190
191     if test $keytype = dsa && test ! -e dsaparam.pem; then
192         echo "Generating DSA parameters, please wait..." >&2
193         openssl dsaparam -out dsaparam.pem $bits 1>&3 2>&3
194     fi
195
196     # Get the current date to add some uniqueness to this certificate
197     curr_date=`date +"%Y %b %d %T"`
198
199     # Create the CAs.
200     for ca in controllerca switchca; do
201         echo "Creating $ca..." >&2
202         oldpwd=$PWD
203         mkdir -p $ca
204         cd $ca
205
206         mkdir -p certs crl newcerts
207         mkdir -p -m 0700 private
208         mkdir -p -m 0733 incoming
209         touch index.txt
210         test -e crlnumber || echo 01 > crlnumber
211         test -e serial || echo 01 > serial
212
213         # Put DSA parameters in directory.
214         if test $keytype = dsa && test ! -e dsaparam.pem; then
215             cp ../dsaparam.pem .
216         fi
217
218         # Write CA configuration file.
219         if test ! -e ca.cnf; then
220             sed "s/@ca@/$ca/g;s/@curr_date@/$curr_date/g" > ca.cnf <<'EOF'
221 [ req ]
222 prompt = no
223 distinguished_name = req_distinguished_name
224
225 [ req_distinguished_name ]
226 C = US
227 ST = CA
228 L = Palo Alto
229 O = Open vSwitch
230 OU = @ca@
231 CN = OVS @ca@ CA Certificate (@curr_date@)
232
233 [ ca ]
234 default_ca = the_ca
235
236 [ the_ca ]
237 dir            = .                     # top dir
238 database       = $dir/index.txt        # index file.
239 new_certs_dir  = $dir/newcerts         # new certs dir
240 certificate    = $dir/cacert.pem       # The CA cert
241 serial         = $dir/serial           # serial no file
242 private_key    = $dir/private/cakey.pem# CA private key
243 RANDFILE       = $dir/private/.rand    # random number file
244 default_days   = 365                   # how long to certify for
245 default_crl_days= 30                   # how long before next CRL
246 default_md     = md5                   # md to use
247 policy         = policy                # default policy
248 email_in_dn    = no                    # Don't add the email into cert DN
249 name_opt       = ca_default            # Subject name display option
250 cert_opt       = ca_default            # Certificate display option
251 copy_extensions = none                 # Don't copy extensions from request
252 unique_subject = no                    # Allow certs with duplicate subjects
253
254 # For the CA policy
255 [ policy ]
256 countryName             = optional
257 stateOrProvinceName     = optional
258 organizationName        = match
259 organizationalUnitName  = optional
260 commonName              = supplied
261 emailAddress            = optional
262 EOF
263         fi
264
265         # Create certificate authority.
266         if test $keytype = dsa; then
267             newkey=dsa:dsaparam.pem
268         else
269             newkey=rsa:$bits
270         fi
271         openssl req -config ca.cnf -nodes \
272             -newkey $newkey -keyout private/cakey.pem -out careq.pem \
273             1>&3 2>&3
274         openssl ca -config ca.cnf -create_serial -out cacert.pem \
275             -days 2191 -batch -keyfile private/cakey.pem -selfsign \
276             -infiles careq.pem 1>&3 2>&3
277         chmod 0700 private/cakey.pem
278
279         cd "$oldpwd"
280     done
281     exit 0
282 fi
283
284 one_arg() {
285     if test -z "$arg1" || test -n "$arg2"; then
286         echo "$0: $command must have exactly one argument; use --help for help" >&2
287         exit 1
288     fi
289 }
290
291 zero_or_one_args() {
292     if test -n "$arg2"; then
293         echo "$0: $command must have zero or one arguments; use --help for help" >&2
294         exit 1
295     fi
296 }
297
298 one_or_two_args() {
299     if test -z "$arg1"; then
300         echo "$0: $command must have one or two arguments; use --help for help" >&2
301         exit 1
302     fi
303 }
304
305 must_not_exist() {
306     if test -e "$1" && test "$force" != "yes"; then
307         echo "$0: $1 already exists and --force not supplied" >&2
308         exit 1
309     fi
310 }
311
312 resolve_prefix() {
313     test -n "$type" || exit 123 # Forgot to call check_type?
314
315     case $1 in
316         ????*)
317             ;;
318         *)
319             echo "Prefix $arg1 is too short (less than 4 hex digits)" >&2
320             exit 0
321             ;;
322     esac
323     
324     fingerprint=$(cd "$pkidir/${type}ca/incoming" && echo "$1"*-req.pem | sed 's/-req\.pem$//')
325     case $fingerprint in
326         "${1}*")
327             echo "No certificate requests matching $1" >&2
328             exit 1
329             ;;
330         *" "*)
331             echo "$1 matches more than one certificate request:" >&2
332             echo $fingerprint | sed 's/ /\
333 /g' >&2
334             exit 1
335             ;;
336         *)
337             # Nothing to do.
338             ;;
339     esac
340     req="$pkidir/${type}ca/incoming/$fingerprint-req.pem"
341     cert="$pkidir/${type}ca/certs/$fingerprint-cert.pem"
342 }
343
344 make_tmpdir() {
345     TMP=/tmp/ovs-pki.tmp$$
346     rm -rf $TMP
347     trap "rm -rf $TMP" 0
348     mkdir -m 0700 $TMP
349 }
350
351 fingerprint() {
352     file=$1
353     name=${1-$2}
354     date=$(date -r $file)
355     if grep -q -e '-BEGIN CERTIFICATE-' "$file"; then
356         fingerprint=$(openssl x509 -noout -in "$file" -fingerprint |
357                       sed 's/SHA1 Fingerprint=//' | tr -d ':')
358     else
359         fingerprint=$(sha1sum "$file" | awk '{print $1}')
360     fi
361     printf "$name\\t$date\\n"
362     case $file in
363         $fingerprint*)
364             printf "\\t(correct fingerprint in filename)\\n"
365             ;;
366         *)
367             printf "\\tfingerprint $fingerprint\\n"
368             ;;
369     esac
370 }
371
372 verify_fingerprint() {
373     fingerprint "$@"
374     if test $batch != yes; then
375         echo "Does fingerprint match? (yes/no)"
376         read answer
377         if test "$answer" != yes; then 
378             echo "Match failure, aborting" >&2
379             exit 1
380         fi
381     fi
382 }
383
384 check_type() {
385     if test x = x"$1"; then
386         type=switch
387     elif test "$1" = switch || test "$1" = controller; then 
388         type=$1
389     else
390         echo "$0: type argument must be 'switch' or 'controller'" >&2
391         exit 1
392     fi
393 }
394
395 parse_age() {
396     number=$(echo $1 | sed 's/^\([0-9]\+\)\([[:alpha:]]\+\)/\1/')
397     unit=$(echo $1 | sed 's/^\([0-9]\+\)\([[:alpha:]]\+\)/\2/')
398     case $unit in
399         s)
400             factor=1
401             ;;
402         min)
403             factor=60
404             ;;
405         h)
406             factor=3600
407             ;;
408         day)
409             factor=86400
410             ;;
411         *)
412             echo "$1: age not in the form Ns, Nmin, Nh, Nday (e.g. 1day)" >&2
413             exit 1
414             ;;
415     esac
416     echo $(($number * $factor))
417 }
418
419 must_exist() {
420     if test ! -e "$1"; then
421         echo "$0: $1 does not exist" >&2
422         exit 1
423     fi
424 }
425
426 pkidir_must_exist() {
427     if test ! -e "$pkidir"; then
428         echo "$0: $pkidir does not exist (need to run 'init' or use '--dir'?)" >&2
429         exit 1
430     elif test ! -d "$pkidir"; then
431         echo "$0: $pkidir is not a directory" >&2
432         exit 1
433     fi
434 }
435
436 make_request() {
437     must_not_exist "$arg1-privkey.pem"
438     must_not_exist "$arg1-req.pem"
439     make_tmpdir
440     cat > "$TMP/req.cnf" <<EOF
441 [ req ]
442 prompt = no
443 distinguished_name = req_distinguished_name
444
445 [ req_distinguished_name ]
446 C = US
447 ST = CA
448 L = Palo Alto
449 O = Open vSwitch
450 OU = Open vSwitch certifier
451 CN = Open vSwitch certificate for $arg1
452 EOF
453     if test $keytype = rsa; then
454         newkey=rsa:$bits
455     else
456         must_exist "$dsaparam"
457         newkey=dsa:$dsaparam
458     fi
459     openssl req -config "$TMP/req.cnf" -text -nodes \
460         -newkey $newkey -keyout "$1-privkey.pem" -out "$1-req.pem" 1>&3 2>&3
461 }
462
463 sign_request() {
464     must_exist "$1"
465     must_not_exist "$2"
466     pkidir_must_exist
467
468     (cd "$pkidir/${type}ca" && 
469      openssl ca -config ca.cnf -batch -in /dev/stdin) \
470         < "$1" > "$2.tmp$$" 2>&3
471     mv "$2.tmp$$" "$2"
472 }
473
474 glob() {
475     files=$(echo $1)
476     if test "$files" != "$1"; then
477         echo "$files"
478     fi
479 }
480
481 exec 3>>$log || true
482 if test "$command" = req; then
483     one_arg
484
485     make_request "$arg1"
486     fingerprint "$arg1-req.pem"
487 elif test "$command" = sign; then
488     one_or_two_args
489     check_type "$arg2"
490     verify_fingerprint "$arg1-req.pem"
491
492     sign_request "$arg1-req.pem" "$arg2-cert.pem"
493 elif test "$command" = req+sign; then
494     one_or_two_args
495     check_type "$arg2"
496
497     pkidir_must_exist
498     make_request "$arg1"
499     sign_request "$arg1-req.pem" "$arg1-cert.pem"
500     fingerprint "$arg1-req.pem"
501 elif test "$command" = verify; then
502     one_or_two_args
503     must_exist "$arg1-cert.pem"
504     check_type "$arg2"
505
506     pkidir_must_exist
507     openssl verify -CAfile "$pkidir/${type}ca/cacert.pem" "$arg1-cert.pem"
508 elif test "$command" = fingerprint; then
509     one_arg
510
511     fingerprint "$arg1"
512 elif test "$command" = self-sign; then
513     one_arg
514     must_exist "$arg1-req.pem"
515     must_exist "$arg1-privkey.pem"
516     must_not_exist "$arg1-cert.pem"
517
518     openssl x509 -in "$arg1-req.pem" -out "$arg1-cert.pem" \
519         -signkey "$arg1-privkey.pem" -req -text 2>&3
520 elif test "$command" = ls; then
521     check_type "$arg2"
522
523     cd "$pkidir/${type}ca/incoming"
524     for file in $(glob "$arg1*-req.pem"); do
525         fingerprint $file
526     done
527 elif test "$command" = flush; then
528     check_type "$arg1"
529
530     rm -f "$pkidir/${type}ca/incoming/"*
531 elif test "$command" = reject; then
532     one_or_two_args
533     check_type "$arg2"
534     resolve_prefix "$arg1"
535
536     rm -f "$req"
537 elif test "$command" = approve; then
538     one_or_two_args
539     check_type "$arg2"
540     resolve_prefix "$arg1"
541
542     make_tmpdir
543     cp "$req" "$TMP/$req"
544     verify_fingerprint "$TMP/$req"
545     sign_request "$TMP/$req"
546     rm -f "$req" "$TMP/$req"
547 elif test "$command" = prompt; then
548     zero_or_one_args
549     check_type "$arg1"
550
551     make_tmpdir
552     cd "$pkidir/${type}ca/incoming"
553     for req in $(glob "*-req.pem"); do
554         cp "$req" "$TMP/$req"
555
556         cert=$(echo "$pkidir/${type}ca/certs/$req" |
557                sed 's/-req.pem/-cert.pem/')
558         if test -f $cert; then
559             echo "Request $req already approved--dropping duplicate request"
560             rm -f "$req" "$TMP/$req"
561             continue
562         fi
563
564         echo
565         echo
566         fingerprint "$TMP/$req" "$req"
567         printf "Disposition for this request (skip/approve/reject)? "
568         read answer
569         case $answer in
570             approve)
571                 echo "Approving $req"
572                 sign_request "$TMP/$req" "$cert"
573                 rm -f "$req" "$TMP/$req"
574                 ;;
575             r*)
576                 echo "Rejecting $req"
577                 rm -f "$req" "$TMP/$req"
578                 ;;
579             *)
580                 echo "Skipping $req"
581                 ;;
582         esac
583     done
584 elif test "$command" = expire; then
585     zero_or_one_args
586     cutoff=$(($(date +%s) - $(parse_age ${arg1-1day})))
587     for type in switch controller; do
588         cd "$pkidir/${type}ca/incoming" || exit 1
589         for file in $(glob "*"); do
590             time=$(date -r "$file" +%s)
591             if test "$time" -lt "$cutoff"; then
592                 rm -f "$file"
593             fi
594         done
595     done
596 else
597     echo "$0: $command command unknown; use --help for help" >&2
598     exit 1
599 fi