#!/usr/local/bin/bash
##
##  gen-ssl-keys.sh - generate SSL key and certificate files.
##
##  Copyright (c) 2004 SIPfoundry, Inc.
##  License by SIPfoundry under the LGPL license.
##  
##  Copyright (c) 2004 Pingtel Corp.
##  Licensed to SIPfoundry under a Contributor Agreement.
##
##  Derived from:
##  CCA -- Trivial Client CA management for testing purposes
##  Copyright (c) 1998-2001 Ralf S. Engelschall, All Rights Reserved. 
##

myName=`basename $0`
myDir=`dirname $0`
# default Certificate Authority Expiration is 3 years
AuthorityDays=3650
# default server expiration is 3 years
CertDays=1095 

CAkeyBits=2048
ServerKeyBits=1024

randomFile=${HOME}/.rnd

# default action - generate self-signed CA certificate and host certificate
Action=DO_ALL
AutoDefault=OFF

#   external tools
openssl="@OPENSSL@"

# default base name for ca and its files
dom=`hostname | sed 's/[^\.]*\.\(.*\)/\1/'`
caName=ca.$dom

# if there is a file of saved default answers for the questions, read them
Defaults=SSL_DEFAULTS
test -f ${Defaults} && . ${Defaults}
############################################################################################
## The variables that can be set from Defaults:
## Name                     Description                             Default Value
##
## countryName              Country Name (2 letter code) 
## stateOrProvinceName      State or Province Name (full name) 
## localityName             Locality Name (eg, city) 
## organizationName         Organization Name (eg, company) 
## organizationalUnitName   Organization Unit Name (eg, section)     VoIP Services
## caName                   CA Common Name (DNS name for CA)         ca.`hostname --domain` 
## caEmail                  Email Contact Address for CA             root@${caName}
## sipDomainName            SIP domain name                          `hostname --domain` 
## server                   Full DNS name for the server             `hostname --fqdn`
## server Server            Common Name (DNS name for Server)        `hostname --fqdn`
## serverEmail              Email Contact Address for Server         ${caEmail}
############################################################################################

function showUsage()
{
    cat <<USAGE

    Generate SSL keys and certificates.

    This script can be used to:

    - A self-signed certificate for a single system:

      gen-ssl-keys.sh

    - A private key and certificate request for use
      with a public or private certificate authority:

      gen-ssl-keys.sh --csr

    - A private self-signed authority certificate you can use to sign
      certificates for multiple systems:

      gen-ssl-keys.sh --new-ca
.
    - a certificate for your server signed by your own authority
      certificate:

      gen-ssl-keys.sh --sign <csr-file>
                      [ --ca <ca-name> ]
                      [ --ca-key <keyfile> ]
.
    See @SIPX_DOCDIR@/INSTALL.ssl for details on usage.

    Copyright (c) 2004 SIPfoundry, Inc.
    Derived from cca.sh:
    Copyright (c) 1998-2001 Ralf S. Engelschall, All Rights Reserved.

USAGE
}

### Prompts for a value for variable, 
###    the default is the current value if variable is set, and the default argument if not
###    the answer is stored in the $Defaults file unless NOSTORE is passed.as the fourth arg.
function askfor () # variable, prompt, default, storeflag
{
    local var="$1"
    local prompt="$2"
    local default="${!var:-$3}"
    local store=$4
    local value=""

    if [ "${AutoDefault}" = "AUTODEFAULT" ]
    then
        value="$default"
        echo "  $prompt: $default"
    else
        until test -n "$value"
        do
            echo -n "$prompt [$default] : "
            read value

            if test -z "$value"
            then
                value="$default"
            fi
        done
    fi

    eval "$var=\"$value\""

    if test "${store}" != "NOSTORE" && ! grep -q -e "^${var}=" ${Defaults} 2> /dev/null
    then
        cat >> ${Defaults} <<EOF
${var}="${value}"
EOF
    fi
}

function seedRand()
{
    # ensures that randomFile has been set up and has new bits in it
    if [ ! -f "${randomFile}" ]
    then
        # find some random files
        # (do not use /dev/random here, because this device 
        # doesn't work as expected on all platforms)
        randfiles=''
        numrandfiles=0
        for file in /var/log/messages /var/adm/messages /tmp/* /etc/resolv.conf; do
            if [ -r $file ]; then
                if [ ".$randfiles" = . ]; then
                    randfiles="$file"
                else
                    randfiles="${randfiles}:$file"
                fi
                numrandfiles=$(($numrandfiles + 1))
                test $numrandfiles -ge 6 && break
            fi
        done
        $openssl rand \
            -rand ${randfiles} \
            -out ${randomFile} \
            ${CAkeyBits} \
            > /dev/null 2>&1
    else
        $openssl rand \
            -rand ${randomFile} \
            -out ${randomFile} \
            ${CAkeyBits} \
            > /dev/null 2>&1
    fi
}

################################################################
#### GET FUNCTIONS
#### These prompt for values from the user,
####  but perform no actual computations.
################################################################

## Get the certificate values for where the subject is.
function getLocality()
{
    askfor countryName            "Country Name (2 letter code)" ""
    askfor stateOrProvinceName    "State or Province Name (full name)" ""
    askfor localityName           "Locality Name (eg, city)" ""
    askfor organizationName       "Organization Name (eg, company)" ""
    askfor organizationalUnitName "Organization Unit Name (eg, section)" "VoIP Services"
}

# Get the CA name and email address
function getCAInfo()
{
    cat <<EOF

______________________________________________________________________
Identifying information for your private Certificate Authority (CA)

EOF

    askfor caName   "CA Common Name (DNS name for CA)" 
    askfor caEmail  "Email Contact Address for CA (name@example.org)" "root@${caName}"
}

# Get the server-specific information
# - ensures that server name is not the same as ca name, which is not allowed by validation
function getServerInfo()
{
    cat <<EOF

______________________________________________________________________
Identifying information for the server:

EOF

    dom=`hostname | sed 's/[^\.]*\.\(.*\)/\1/'`
    askfor sipDomainName "SIP domain name" $dom
    askfor server        "Full DNS name for the server" `hostname` NOSTORE
    while test ${caName} = ${server}
      do
      echo "" 1>&2
      echo "Error: The Server name must not be the same as the CA name." 1>&2
      server=""
      askfor server "Server Common Name (DNS name for Server)" `hostname` NOSTORE
    done

    askfor serverEmail "Email Contact Address for Server (name@example.org)" "${caEmail}" NOSTORE
}

################################################################
#### GENERATOR FUNTIONS
#### These perform calculations and create files, 
#### They never prompt for anything, so that they can be combined
#### by different action functions.
################################################################

## Generate a self-signed CA certificate
function genCA()
{
    cat <<EOF

Generating private Certificate Authority (CA)
______________________________________________________________________

        Generating RSA private key for CA (${CAkeyBits} bit)

EOF
    $openssl genrsa \
        -rand ${randomFile} \
        -out ${caName}.key \
        ${CAkeyBits} \
        > /dev/null 2> /dev/null

    if [ $? -ne 0 ]; then
        echo "${myName}:Error: Failed to generate RSA private key" 1>&2
        exit 1
    fi
    chmod go= ${caName}.key

    echo "______________________________________________________________________"
    echo ""
    echo "        Generating X.509 certificate signing request for CA"

    cat >${caName}_csr.cfg <<EOT
[ req ]
default_bits                    = ${CAkeyBits}
distinguished_name              = req_DN
prompt                          = no
RANDFILE                        = ${randomFile}
[ req_DN ]
countryName                     = ${countryName}
stateOrProvinceName             = ${stateOrProvinceName}
localityName                    = ${localityName}
0.organizationName              = ${organizationName}
organizationalUnitName          = ${organizationalUnitName}
commonName                      = ${caName}
emailAddress                    = ${caEmail}
EOT

    $openssl req \
        -config ${caName}_csr.cfg \
        -new \
        -key ${caName}.key \
        -sha1 \
        -out ${caName}.csr \
        > /dev/null

    if [ $? -ne 0 ]; then
        echo "${myName}:Error: Failed to generate certificate signing request" 1>&2
        exit 1
    fi
    echo "______________________________________________________________________"
    echo ""
    echo "        Generating X.509 certificate for CA signed by itself"

    # use minutes in the epoch as the initial serial number
    # to prevent clashes when the admin wipes out the system and starts over.
    startSerial=$((`date +%s` / 60 ))

    cat >${caName}_crt.cfg <<EOT
[ x509v3 ]
basicConstraints = CA:true,pathlen:0
nsComment        = "${myName} generated custom CA certificate"
nsCertType       = sslCA
EOT

    $openssl x509 \
        -req \
        -signkey ${caName}.key \
        -set_serial ${startSerial} \
        -extfile ${caName}_crt.cfg \
        -extensions x509v3 \
        -days ${AuthorityDays} \
        -sha1 \
        -in ${caName}.csr \
        -out ${caName}.crt \
        -text \
        > /dev/null 2>&1

    if [ $? -ne 0 ]; then
        echo "${myName}:Error: Failed to generate self-signed CA certificate" 1>&2
        exit 1
    fi
    printf "%08x" $((${startSerial} + 1)) > ${caName}.ser

    echo "______________________________________________________________________"
    echo ""
    echo -n "Verify..."
    $openssl verify ${caName}.crt > /dev/null 2>&1

    if [ $? -eq 0 ]; then
        echo "CA certificate OK" 1>&2
        rm -f ${caName}.csr  ${caName}_csr.cfg  ${caName}_crt.cfg
    else
        echo "${myName}:Error: Failed to verify resulting X.509 certificate" 1>&2
        exit 1
    fi
}

## Generate a Certificate Signing Request
function genServerCSR()
{
    echo ""
    echo "Generating server certificate request [$server]"
    echo "______________________________________________________________________"
    echo ""
    echo "        Generating RSA private key for server (${ServerKeyBits} bit)"

    $openssl genrsa \
        -rand ${randomFile} \
        -out $server.key \
        ${ServerKeyBits} \
        > /dev/null 2>&1

    if [ $? -ne 0 ]; then
        echo "${myName}:Error: Failed to generate RSA private key" 1>&2
        exit 1
    fi
    chmod go= $server.key

    echo "______________________________________________________________________"
    echo ""
    echo "        Generating X.509 certificate signing request for '${server}'"
    cat >${server}_csr.cfg <<EOT
[ req ]
default_bits           = ${ServerKeyBits}
distinguished_name     = req_DN
prompt                 = no
RANDFILE               = ${randomFile}
[ req_DN ]
countryName            = ${countryName}
stateOrProvinceName    = ${stateOrProvinceName}
localityName           = ${localityName}
0.organizationName     = ${organizationName}
organizationalUnitName = ${organizationalUnitName}
commonName             = ${server}
emailAddress           = ${serverEmail}
[ extend_req ]
basicConstraints       = CA:FALSE
subjectAltName         = URI:sip:${sipDomainName},DNS:${server}
subjectKeyIdentifier   = hash
EOT

    $openssl req \
        -config ${server}_csr.cfg \
        -new \
        -sha1 \
        -key $server.key \
        -reqexts extend_req \
        -out ${server}.csr \
        -days ${CertDays} \
        > /dev/null
       
    if [ $? -ne 0 ]; then
        echo "${myName}:Error: Failed to generate certificate signing request" 1>&2
        exit 1
    else
        rm -f  ${server}_csr.cfg
    fi
}

## Sign a server certificate
function signServerCertificate()
{
    echo "______________________________________________________________________"
    echo ""
    echo "        Generating X.509 certificate signed by ${caName}"

    cat >${server}_crt.cfg <<EOT
[ x509v3 ]
basicConstraints = CA:false,pathlen:0
[ extend_cert ]
basicConstraints       = CA:FALSE
subjectAltName         = URI:sip:${sipDomainName},DNS:${server}
subjectKeyIdentifier   = hash
EOT

    $openssl x509 \
        -req \
        -CA ${caName}.crt \
        -CAkey ${caKey:-${caName}.key} \
        -CAserial ${caName}.ser \
        -extfile ${server}_crt.cfg \
        -extensions x509v3 \
        -extensions extend_cert \
        -sha1 \
        -days ${CertDays} \
        -in ${server}.csr \
        -out ${server}.crt \
        -text \
        > /dev/null 2>&1

    if [ $? -ne 0 ]; then
        echo "${myName}:Error: Failed to generate X.509 certificate" 1>&2
        exit 1
    fi

    ## how to generate pkcs12 certs...
    #caname="`$openssl x509 -noout -text -in ${caName}.crt |\
    #         grep Subject: | sed -e 's;.*CN=;;' -e 's;/Em.*;;'`"
    #servername="`$openssl x509 -noout -text -in $server.crt |\
    #           grep Subject: | sed -e 's;.*CN=;;' -e 's;/Em.*;;'`"
    #echo "Assembling PKCS#12 package"
    #$openssl pkcs12 -export -in $server.crt -inkey $server.key -certfile ${caName}.crt -name "$servername" -caname "$caname" -out $server.p12

    echo "______________________________________________________________________"
    echo ""
    echo -n "Verify..."
    $openssl verify \
        -CAfile ${caName}.crt \
        $server.crt \
        > /dev/null

    if [ $? -eq 0 ]; then
        echo "Server certificate OK" 1>&2
        rm -f ${server}_crt.cfg
    else
        echo "${myName}:Error: Failed to verify resulting X.509 certificate" 1>&2
        exit 1
    fi
}


################################################################
#### ACTION FUNCTIONS
#### These combine prompting the user with actually doing things.
################################################################

#### Do the one-step self-signed CA and a certificate from it.
function doAll() 
{
    cat <<EOF

  We need some information from you to generate the certificates: 

EOF

    getLocality
    getCAInfo
    getServerInfo

    seedRand
    
    genCA
    genServerCSR
    signServerCertificate

    cat <<EOF

  To install your certificate, run the following command 
  as root on the server:

    @bindir@/ssl-cert/install-cert.sh

EOF
}

################################################################
#### Create a CA
################################################################
function doNewCA() 
{
    cat <<EOF

  We need some information from you to set up your Certificate Authority:

EOF

    getLocality
    getCAInfo

    seedRand
    genCA

    cert_expires=`@OPENSSL@ x509 -in "${caName}.crt" -noout -enddate | sed 's/notAfter=//'`

    cat <<EOF

  These files have been created for your private Certificate Authority:

       The root certificate of your CA:   ${caName}.crt 

       You will need to make the ${caName}.crt file available to 
       anyone who wishes to be able to validate certificates signed 
       by your CA.

             The secret key of your CA:   ${caName}.key

       You MUST keep the ${caName}.key file as secure as possible; 
       it is the foundation of the security of your CA, and by extension
       anything using certificates signed by your CA.  You may move this
       do removable media; if you do, then when signing keys you must
       specify the full path to the key using the --ca-key switch.

   The serial number store for your CA:   ${caName}.ser

       This is used to ensure uniqueness when you regenerate certificates.

  The CA certificate will expire ${cert_expires}; 
  you will need to rerun this command to generate a new certificate
  then (or, preferably, before then).
  
EOF
}

################################################################
#### CSR
################################################################
function doCSR() 
{
    cat <<EOF

  We need some information from you to generate the certificate signing request: 

EOF

    getLocality
    getServerInfo

    seedRand
    genServerCSR

    cat <<EOF
______________________________________________________________________
Results:

   server key:         $server.key
   server CSR:         $server.csr

  To obtain your certificate from a Certificate Authority, send the
  $server.csr file to your CA for signing.  When you get the response,
  put the certificate data they give you into $server.crt, and then run
  this command as root in this directory:

      @bindir@/ssl-cert/install-cert.sh $server

  You may also need to install the root certificate from the CA; if this
  is needed, the install-cert.sh command will detect this and provide 
  instructions.

EOF

}

## Extract information from a CSR and present it for approval 
function checkCSR()
{
    server=`@OPENSSL@ req -in "${csrFile}" -subject -nameopt RFC2253,multiline -noout | perl -ne 'use English; m/^\s+commonName\s+=\s+/ && print $POSTMATCH'`

    if [ -z "${server}" ]
    then
        echo "${myName}:Error: unable to read name in certificate signing request." 1>&2
        exit 1
    fi

    cat <<EOF

 The data for the requested certificate is:

EOF
    echo ""
    $openssl req \
        -in ${csrFile} \
        -text \
        -nameopt RFC2253,multiline \
        -noout \
    | perl -n -e '/^\s+([0-9a-f]{2}:)+([0-9a-f]{2})?$/i || print;' 

    cat <<EOF

 Examine the information above carefully.

 Do you wish to sign a certificate with the above
       for server ${server}
   with authority ${caName}

EOF
   
    answer=""
    until [ "${answer}" = "yes" -o "${answer}" = "no" ]; do
        echo -n "   Sign Certificate? (yes|no): "
        read answer
    done

    test "${answer}" = "no" && exit
}

################################################################
#### Use a CSR to create a Certificate
################################################################
function doSignServer() 
{
    if [ -z "${csrFile}" ]
    then
        echo "Please specify a CSR file name" 1>&2
        showUsage
        exit 1
    fi
    
    checkCSR 
    signServerCertificate

    cert_expires=`@OPENSSL@ x509 -in "${server}.crt" -noout -enddate | sed 's/notAfter=//'`

    cat <<EOF
______________________________________________________________________


   CA certificate:     ${caName}.crt
   server certificate: $server.crt

  To install these certificates and the server key on this system, run the
  following command as root in this directory:

      @bindir@/ssl-cert/install-cert.sh $server

  The server certificate will expire ${cert_expires}; 
  you will need to rerun this command to generate new certificates
  then (or, preferably, before then).  You do not need to create a 
  new .csr file - you can just resign the same file.

EOF
}


## parse arguments
while [ $# -ne 0 ]
do
    case ${1} in

        --csr)
            ##
            ## Create a key and a Certificate Signing Request
            ##
            Action=DO_CSR
            ;;

        --new-ca)
            ##
            ## Create a key and a Certificate Signing Request
            ##
            Action=DO_CA
            ;;

        --ca)
            ##
            ## Certificate Authority Name
            ##
            if [ $# -lt 2 ]
            then
                echo "Must specify <ca-name> with ${1}" 1>&2
                Action=USAGE
                break
            else
                caName=${2}
                shift # consume the switch ( for n values, consume n-1 )
            fi
            ;;

        --ca-key)
            ##
            ## Certificate Authority Key File
            ##
            if [ $# -lt 2 ]
            then
                echo "Must specify <ca-key-file> with ${1}" 1>&2
                Action=USAGE
                break
            else
                caKey=${2}
                shift # consume the switch ( for n values, consume n-1 )
            fi
            ;;

        --sign)
            ##
            ## Sign a CSR to produce a Server Certificate
            ##
            if [ $# -lt 2 ]
            then
                echo "Must specify <csr-file> with ${1}" 1>&2
                Action=USAGE
                break
            else
                csrFile=${2}
                Action=DO_SERVER
                shift # consume the switch ( for n values, consume n-1 )
            fi
            ;;

        -d)
            AutoDefault=AUTODEFAULT
            ;;

        *)
            ##
            ## unrecognized argument
            ##
            Action=USAGE
            break
            ;;
    esac           

    shift # always consume 1
done

case ${Action} in

    DO_ALL)
        doAll
        ;;

    DO_CA)
        doNewCA
        ;;

    DO_CSR)
        doCSR
        ;;

    DO_SERVER)
        doSignServer
        ;;

    *)
        showUsage
        exit 1
        ;;
esac

exit
