scripts/ssl_mgmt/ssl_mgmt

465 lines
13 KiB
Plaintext
Raw Normal View History

2012-02-20 17:23:33 +01:00
#!/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 <http://www.gnu.org/licenses/>.
#
# 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 { <service> | <certificate file> }"
2012-02-20 17:23:33 +01:00
echo "$progname -h"
echo
echo "First form renew the certificate specified as a file or a service name"
2012-02-20 17:23:33 +01:00
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
2012-02-20 17:23:33 +01:00
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 ()
2012-02-20 17:23:33 +01:00
{
local - user domain action
config_only=""
no_overwrite=""
while getopts "cgh" opt
2012-02-20 17:23:33 +01:00
do
case $opt in
"c")
config_only=yes ;;
"g")
no_overwrite=yes ;;
2012-02-20 17:23:33 +01:00
"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" ]
2012-02-20 17:23:33 +01:00
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 ()
2012-02-20 17:23:33 +01:00
{
accessedFile="$1"
accessMode="$2"
case $accessMode in
"READ")
[ -r $accessedFile ];;
"WRITE")
[ -w $accessedFile ];;
esac
if [ ! $? -eq 0 ]
2012-02-20 17:23:33 +01:00
then
echo "You do not have enough rights to access ${accessedFile}."
echo "Permission of $accessedFile are:"
getfacl "$accessedFile"
exit 1
2012-02-20 17:23:33 +01:00
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
2012-02-20 17:23:33 +01:00
}
# @param subject the subject line
# @param field the field name
#
# Get a subject field value from the subject line
2012-02-20 17:23:33 +01:00
get_field_from_line ()
{
local - line field result
line="$1"
field="$2"
result="$(echo ${line} | sed -r "s/.*${field} *= *//")"
if [ "$result" != "$line" ]
then
echo "${result%%,*}"
fi
2012-02-20 17:23:33 +01:00
}
# @param certPath the absolute path to the certificate to renew
#
# Get configuration values to fill openssl.cnf with
2012-02-20 17:23:33 +01:00
get_cert_params ()
{
local - subject issuer dates ext fromDate toDate certPath
2012-02-20 17:23:33 +01:00
certPath="$1"
subject="$(openssl x509 -in "$certPath" -noout -subject)"
dates="$(openssl x509 -in "$certPath" -noout -dates)"
2012-02-20 17:23:33 +01:00
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
2012-02-20 17:23:33 +01:00
do
if [ "$ext" = "X509v3 Subject Alternative Name: " ]
2012-02-20 17:23:33 +01:00
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
2012-02-20 17:23:33 +01:00
add_to_replace_cmd ()
{
local - replaceCmd key value
replaceCmd="$1"
key="$2"
value="$3"
echo "$replaceCmd${replaceCmd:+;}s/$key/${value:-}/"
2012-02-20 17:23:33 +01:00
}
# Generate the openssl.cnf configuration file from the openssl.cnf.in template
2012-02-20 17:23:33 +01:00
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
2012-02-20 17:23:33 +01:00
if ask_user_default_no "Do you want to edit the openssl configuration file?"
then
2014-03-09 09:54:29 +01:00
if [ -n "${EDITOR:-}" ]
2012-02-20 17:23:33 +01:00
then
$EDITOR $opensslCnfTmpFile
2012-02-20 17:23:33 +01:00
else
editor $opensslCnfTmpFile
2012-02-20 17:23:33 +01:00
fi
fi
mv $opensslCnfTmpFile $opensslCnfFile
2012-02-20 17:23:33 +01:00
}
# @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
2012-02-20 17:23:33 +01:00
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
2012-02-20 17:23:33 +01:00
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 <<EOF
$notifyTemplate
EOF
fi
fi
2012-02-20 17:23:33 +01:00
return 0
}
main ()
{
local - ret servicesok certPath keyPath
2012-02-20 17:23:33 +01:00
ret=0
parse_args "$@"
set_variables
cd $workDir
2012-02-20 17:23:33 +01:00
if [ "${service}" = "all" ]
then
if [ -z "$managedCerts" ]
then
echo -n "You need to set managedCerts for renew" >&2
echo " all to work" >&2
fi
exit_if_no_access "$managedCerts" "READ"
2012-02-20 17:23:33 +01:00
services=""
for service in $managedCerts
2012-02-20 17:23:33 +01:00
do
services="$services $service"
done
2012-02-20 17:23:33 +01:00
else
services=${service}
fi
exit_if_no_access "$certDestDir" "WRITE"
exit_if_no_access "$keyDestDir" "WRITE"
2012-02-20 17:23:33 +01:00
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"
2012-02-20 17:23:33 +01:00
generate_config
if [ -n "$config_only" ]
then
continue
fi
if ! generate_cert "$service" "$certPath" "$keyPath"
2012-02-20 17:23:33 +01:00
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
2012-02-20 17:23:33 +01:00
return $ret
}
main "${@:-""}"
exit $?