#!/bin/sh # # ssl_mgmt, Copyright © 2012 Thomas Preud'homme # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # email_account is a helper to create e-mail accounts in a configuration # using Postfix mail transport agent with vhosts and Cyrus IMAP server, # in a Debian environment. set -u # Display usage. usage () { local - progname progname=$1 echo "Usage :" echo echo "$progname [-c | -g] renew { | }" echo "$progname -h" echo echo "First form renew the certificate specified as a file or a service name" echo echo "Possible option:" echo echo "-c Only generate the configuration" echo "-g Stop after generating the certificate and keys: do not overwrite" echo " existing ones" echo echo "Second form prints this help." } # @param question Question to ask # @return 0 if answer is positive, 1 else # # Ask question to the user with a default choice set to no. ask_user_default_no () { local - answer answer="unset" echo -n "$1 [y/N] " while [ -n "$answer" -a "$answer" != "y" -a "$answer" != "Y" -a "$answer" != "n" -a "$answer" != "N" ] do read answer if [ -z "$answer" -o "$answer" = "n" -o "$answer" = "N" ] then return 1 fi if [ "$answer" = "y" -o "$answer" = "Y" ] then break fi echo -n "$2 [y/N] " done return 0 } # Parse arguments and test their number and value is correct. parse_args () { local - user domain action config_only="" no_overwrite="" while getopts "cgh" opt do case $opt in "c") config_only=yes ;; "g") no_overwrite=yes ;; "h") if [ $# -gt 1 ] then echo "Error! Too many arguments." >&2 exit 1 fi usage $(basename "$0") exit 0 ;; esac done eval action="\${$OPTIND:-}" if [ $(($#-$OPTIND+1)) -ne 2 -o "$action" != "renew" ] then usage $(basename "$0") exit 0 fi eval service="\$$((OPTIND+1))" } # @param file the file we wish to access # @param mode the mode we wish to access the file in. # It must be either "READ" or "WRITE". # # Exit if we are unable to access the given file with requested access mode # NB: this function does not return. exit_if_no_access () { accessedFile="$1" accessMode="$2" case $accessMode in "READ") [ -r $accessedFile ];; "WRITE") [ -w $accessedFile ];; esac if [ ! $? -eq 0 ] then echo "You do not have enough rights to access ${accessedFile}." echo "Permission of $accessedFile are:" getfacl "$accessedFile" exit 1 fi } # Set all variables configuring the overall behavior of ssl_mgmt. A default # value is provided and overriden if set in the configuration file set_variables () { cnfFilePath=${cnfFilePath:-/etc/${0##*/}.conf} exit_if_no_access "$cnfFilePath" "READ" . $cnfFilePath workDir=${workDir:-${0%/*/*}/lib/${0##*/}} csrSubdir=${csrSubdir:-csr} certSubdir=${certSubdir:-newcerts} keySubdir=${keySubdir:-newkeys} certDestDir=${certDestDir:-/etc/ssl/certs} keyDestDir=${keyDestDir:-/etc/ssl/private} CACertPath=${CACertPath:-$certDestDir/ca-cert.pem} CAKeyPath=${CAKeyPath:-$keyDestDir/ca-key.pem} opensslCnfFile=openssl.cnf if [ -z "${rootCAPwdPath:-}" ] then echo -n "You must set rootCAPwdPath to the file containing" >&2 echo " the root CA password" >&2 fi managedCerts=${managedCerts:-} notifiedUsers=${notifiedUsers:-} if [ -n "${notifiedUsers}" -a -z "${keyId:-}" ] then echo -n "You must set keyId to the ID of the key to sign" >&2 echo " the message sent to users to be" >&2 echo "notified of new certificate." >&2 fi notifySubject=${notifySubject:-'New fingerprint for service $service'} if [ -z "${notifyTemplate:-}" ] then notifyTemplate='Certificate for $service has changed. The fingerprint of the new certificate is: $fingerprint' fi } # @param subject the subject line # @param field the field name # # Get a subject field value from the subject line get_field_from_line () { local - line field result line="$1" field="$2" result="${line#*$field=}" if [ "$result" != "$line" ] then echo "${result%%/*}" fi } # @param certPath the absolute path to the certificate to renew # # Get configuration values to fill openssl.cnf with get_cert_params () { local - subject issuer dates ext fromDate toDate certPath certPath="$1" subject="$(openssl x509 -in "$certPath" -noout -subject)" dates="$(openssl x509 -in "$certPath" -noout -dates)" exclNoExt="-certopt no_header,no_version,no_serial,no_signame" exclNoExt="$exclNoExt,no_validity,no_subject,no_issuer,no_pubkey" exclNoExt="$exclNoExt,no_sigdump,no_aux" altName="$(openssl x509 -in "$certPath" -text $exclNoExt | while read ext do if [ "$ext" = "X509v3 Subject Alternative Name: " ] then read altName echo $altName break fi done)" country=$(get_field_from_line "$subject" "C") state=$(get_field_from_line "$subject" "ST") city=$(get_field_from_line "$subject" "L") organization=$(get_field_from_line "$subject" "O") unit=$(get_field_from_line "$subject" "OU") commonName=$(get_field_from_line "$subject" "CN") fromDate=${dates#*notBefore=} fromDate=${fromDate%notAfter*} fromDate=$(date -d "$fromDate" "+%s") toDate=${dates#*notAfter=} toDate=$(date -d "$toDate" "+%s") days=$(($toDate-$fromDate)) days=$(($days/86400)) } # @param cmd the current sed replace command # @param key the pattern to be replaced # @param value the value to replace the pattern by # # Add a replace command s/key/value to the sed replace command passed in # argument add_to_replace_cmd () { local - replaceCmd key value replaceCmd="$1" key="$2" value="$3" echo "$replaceCmd${replaceCmd:+;}s/$key/${value:-}/" } # Generate the openssl.cnf configuration file from the openssl.cnf.in template generate_config () { local - replaceCmd cnfTmpFile replaceCmd="$(add_to_replace_cmd "${replaceCmd:-}" "@LENGTH@" "${days:-}")" replaceCmd="$(add_to_replace_cmd "$replaceCmd" "@ORG@" "${organization:-}")" replaceCmd="$(add_to_replace_cmd "$replaceCmd" "@ORGUNIT@" "${unit:-}")" replaceCmd="$(add_to_replace_cmd "$replaceCmd" "@LOCALITY@" "${city:-}")" replaceCmd="$(add_to_replace_cmd "$replaceCmd" "@STATE@" "${state:-}")" replaceCmd="$(add_to_replace_cmd "$replaceCmd" "@COUNTRY@" "${country:-}")" replaceCmd="$(add_to_replace_cmd "$replaceCmd" "@COMMONNAME@" "${commonName:-}")" replaceCmd="$(add_to_replace_cmd "$replaceCmd" "@ALTNAME@" "${altName:-}")" replaceCmd="$replaceCmd${replaceCmd:+;}s/\(.*=[[:blank:]]*\$\)/#\\1/" opensslCnfTmpFile="$(mktemp --tmpdir=. openssl.cnf.XXXXXXXXXX)" sed "$replaceCmd" $opensslCnfFile.in > $opensslCnfTmpFile if ask_user_default_no "Do you want to edit the openssl configuration file?" then if [ -n "${EDITOR:-}" ] then $EDITOR $opensslCnfTmpFile else editor $opensslCnfTmpFile fi fi mv $opensslCnfTmpFile $opensslCnfFile } # @param service the name of the service associated with the certificate to # renew # @param certPath the absolute path to the certificate to renew # @param keyPath the absolute path to the key associated with the certificate # to renew # # Generate the certificate, key and combine key+certificate based on the values # of the existing certificate generate_cert () { local - service certPath keyPath reqFile certFile keyFile keycertFile service="$1" certPath="$2" keyPath="$3" reqFile=${service}-req.pem certFile=${certPath##*/} keyFile=${keyPath##*/} keycertFile=${service}-keycert.pem keycertPath=${keyPath%/*}/$keycertFile # Create the CSR and the key openssl req -new -nodes -out $csrSubdir/$reqFile -keyout $keySubdir/$keyFile -config $opensslCnfFile if ! openssl req -in $csrSubdir/$reqFile -text -verify -noout 2>/dev/null then echo "Generated CSR is corrupted." >&2 rm $csrSubdir/$reqFile $keySubdir/$keyFile return 1 fi if ! ask_user_default_no "Is the Certificate Signing Request correct?" then return 1 fi # Sets ownership and access rights of the key getfacl "$keyPath" | setfacl --set-file=- $keySubdir/$keyFile chown --reference="$keyPath" $keySubdir/$keyFile # Sign the CSR to make a certificate openssl ca -batch -config $opensslCnfFile -cert $CACertPath \ -keyfile $CAKeyPath -passin file:$rootCAPwdPath \ -out $certSubdir/$certFile -infiles $csrSubdir/$reqFile # Create the keycert file (file with merged key and certificate) and # sets its ownership and access rights cat $keySubdir/$keyFile $certSubdir/$certFile > $keySubdir/$keycertFile getfacl "$keycertPath" | setfacl --set-file=- $keySubdir/$keycertFile chown --reference="$keycertPath" $keySubdir/$keycertFile # Safety check if ! openssl x509 -noout -text -in $certSubdir/$certFile >/dev/null 2>&1 || ! openssl verify -CAfile $CACertPath $certSubdir/$certFile >/dev/null 2>&1 then echo "Generated certificate is corrupted." >&2 rm $certSubdir/$certFile $keySubdir/$keyFile $keySubdir/$keycertFile return 1 fi if ! openssl rsa -noout -text -in $keySubdir/$keyFile >/dev/null 2>&1 then echo "Generated key is corrupted." >&2 rm $certSubdir/$certFile $keySubdir/$keyFile return 1 fi certModulus=$(openssl x509 -noout -modulus -in $certSubdir/$certFile) keyModulus=$(openssl rsa -noout -modulus -in $keySubdir/$keyFile) if [ -z "$certModulus" -o "$certModulus" != "$keyModulus" ] then echo -n "Generated certificate and key do not match." >&2 echo " Aborting." >&2 rm $certSubdir/$certFile $keySubdir/$keyFile $keySubdir/$keycertFile return 1 fi # Sets ownership and access rights of the certificate getfacl "$certPath" | setfacl --set-file=- $certSubdir/$certFile chown --reference="$certPath" $certSubdir/$certFile # Notify and install the new certificate if [ -z "$no_overwrite" ] then if [ ! -f "$certDestDir/$certFile" ] then echo "No file named $certFile in directory $certDestDir:" >&2 echo "there might be a problem" >&2 fi if [ ! -f "$keyDestDir/$keyFile" ] then echo "Error! No file named $keyFile in directory $keyDestDir:" >&2 echo "there might be a problem." >&2 fi if [ ! -f "$keyDestDir/$keycertFile" ] then echo "Error! No file named $keycertFile in directory $keyDestDir:" >&2 echo "there might be a problem." >&2 fi mv $keySubdir/$keyFile $keyDestDir mv $keySubdir/$keycertFile $keyDestDir mv $certSubdir/$certFile $certDestDir fingerprint="$(openssl x509 -in "$certPath" -noout -fingerprint)" fingerprint=${fingerprint#*=} if [ -n "$notifiedUsers" -a -n "$keyId" ] then eval notifySubject="\"$notifySubject\"" eval notifyTemplate="\"$notifyTemplate\"" if [ -z "${keyPwdPath:-}" ] then pwdOpt="--passphrase-fd 3" pwdRedir='3<&0' else pwdOpt="--passphrase-file $keyPwdPath" pwdRedir="" fi { gpg -u $keyId --clearsign -a $pwdOpt \ | mail -s "$notifySubject" $notifiedUsers ; } \ 3<&0 <&2 echo " all to work" >&2 fi exit_if_no_access "$managedCerts" "READ" services="" for service in $managedCerts do services="$services $service" done else services=${service} fi exit_if_no_access "$certDestDir" "WRITE" exit_if_no_access "$keyDestDir" "WRITE" for service in $services do servicesok="" certPath="$service" if [ -f "$certPath" ] then service="${service##*/}" service="${service%.*}" keyPath="$keyDestDir/${service}.key" else certPath="$certDestDir/${service}-cert.pem" keyPath="$keyDestDir/${service}-key.pem" fi if [ ! -f "$certPath" ] then ret=1 continue fi exit_if_no_access "$certPath" "READ" exit_if_no_access "$keyPath" "READ" exit_if_no_access "$rootCAPwdPath" "READ" get_cert_params "$certPath" generate_config if [ -n "$config_only" ] then continue fi if ! generate_cert "$service" "$certPath" "$keyPath" then ret=1 else servicesok="$servicesok${servicesok:+ }$service" fi done if [ -z "$config_only" ] then if [ -n "$servicesok" ] then echo "You should restart the following services: $servicesok" else echo "No certificate generated" fi fi return $ret } main "${@:-""}" exit $?