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