465 lines
12 KiB
Bash
Executable File
465 lines
12 KiB
Bash
Executable File
#!/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> }"
|
|
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 <<EOF
|
|
$notifyTemplate
|
|
EOF
|
|
fi
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
main ()
|
|
{
|
|
local - ret servicesok certPath keyPath
|
|
ret=0
|
|
parse_args "$@"
|
|
set_variables
|
|
cd $workDir
|
|
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"
|
|
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 $?
|