Table des matières

EJBCA - Création de certificats TLS

CréationNicolas THOREZ 2024/03/20 17:03

Grâce à nos autorités de certification, nous avons la possibilité de créer des certificats TLS pour nos serveurs webs. Ces certificats et le fait que les autorités sont reconnues par les postes clients permettent de se passer des certificats auto-signés et par extensions, suppriment les messages d'erreurs lors de l'accès à des sites auto-signés :

On va donc voir comment créer un modèle de certificat TLS et les détails de la création de certificats pour des serveurs webs.

Réf. : EJBCA

Création du modèle de certificat

Création du modèle de serveur

Opérationnel

Voilà, notre autorité de certification est configurée et opérationnelle pour la délivrance de certificat TLS pour nos serveurs webs internes. Il ne reste plus qu'à créer ces fameux certificats. Pour cela, nous avons deux possibilités :
  • Via l'interface web
  • Via des appels API

Création d'un certificat TLS

Via l'interface

nano /tmp/openssl.conf

[ req ]
default_md = sha256
prompt = no
distinguished_name = dn

[ dn ]
CN = <FQDN du serveur pour lequel on souhaite créer un certificat>
O = <Mon organisation>
C = <Pays de mon organisation>

openssl ecparam -genkey -name prime256v1 -out <fqdn>.key

openssl req -new -key <fqdn>.key -config /tmp/openssl.conf

Par appel API

#!/bin/bash

#########################################
#                                       #
#   Script de gestion des certificats   #
#                                       #
#########################################

#===========#
# Variables #
#===========#

# Dossier de travail
WorkingDirectory=/usr/local/batch/certmgr

# Authentification pour le script
ScriptAuthentication="$WorkingDirectory/script.p12:123456"

# Adresse de l'autorité de certification
AuthorityAddress="localhost"

# Profil de certificat
CertificateProfileName="TLS-SERVER"

# Profil de serveur
EndEntityProfileName="TlsServer"

# Nom de l'autorité de certification
CertificationAuthorityName="SubCa"

# Fichier de configuration pour openssl
OpensslConfig="/tmp/cert.conf"

# Modèle de requête API
ApiTemplate='{"certificate_request":$ApiCsr, "certificate_profile_name":$ApiCp, "end_entity_profile_name":$ApiEep, "certificate_authority_name":$ApiCa, "username":$ApiUser, "password":$ApiPwd}'

# Dossier racine pour les certificats
CertificateRoorDirectory="$WorkingDirectory/issued"

# Utilisateur pour l'enregistremet
EnrollmentUser="exploit"

# Mot de passe pour l'enregistrement
EnrollmentPassword="123456"

# Fichier de réponse de l'API
ApiResponse="/tmp/api.json"

# Certificat racine
RootCertificate="$WorkingDirectory/issued/RootCa.pem"

# Certificat intermédiaire
SubCertificate="$WorkingDirectory/issued/SubCa.pem"

# Fichier de log
ManageLog="/var/log/manage-certificate.log"

# Raison de révocation par défaut
RevokeReason="CESSATION_OF_OPERATION"

# Organisation
Organization="ShyrkaSystem"

# Pays
Country="FR"

#===========#
# Fonctions #
#===========#

# Demande d'aide
display-help() {
        echo "Script de gestion des certificats"
        echo "---------------------------------"
        echo ""
        echo "Usage : ./manage-certificate.sh [add|new|create] <fqdn>"
        echo "                                [renew] <fqdn>"
        echo "                                [revoke|delete] <fqdn> <reason>"
        echo "                                [status] <fqdn>"
        echo "                                [help]"
        echo ""
        echo "Arguments :"
        echo ""
        echo "add                   Crée un certificat pour le domaine <fqdn>."
        echo "create                Identique à add."
        echo "delete                Révoque et supprime le certificat pour le domaine <fqdn>."
        echo "help                  Affiche l'aide."
        echo "new                   Identique à add."
        echo "renew                 Renouvelle un certificat existant (création si inexistant)."
        echo "revoke                Identique à delete."
        echo "status                Affiche le status du certificat pour le domaine <fqdn>."
        echo ""
        echo "<fqdn>                Obligatoire pour toute action et sous la forme d'un FQDN (ex : www.google.fr)."
        echo "<reason>              Facultatif. Indique la raison de la révocation du certificat."
        echo "                      Par défaut, la raison sera CESSATION_OF_OPERATION."
        echo "                      Les raisons disponibles sont :"
        echo "                        - KEY_COMPROMISE"
        echo "                        - CA_COMPROMISE"
        echo "                        - AFFILIATION_CHANGED"
        echo "                        - SUPERSEDED"
        echo "                        - CESSATION_OF_OPERATION"
        echo "                        - CERTIFICATE_HOLD"
        echo "                        - PRIVILEGES_WITHDRAW"
        echo "                        - AA_COMPROMISE"
        echo ""
}

# Identifiant d'un certificat
get-id() {
        openssl x509 -noout -serial -in $CertificateRoorDirectory/$1/fullchain.pem | cut -d'=' -f2 | sed 's/://g'
}

# Status d'un certificat
get-status() {
        curl -X GET -k -s \
                --cert-type P12 \
                --cert "$ScriptAuthentication" \
                --header "accept: application/json" \
                "https://${AuthorityAddress}/ejbca/ejbca-rest-api/v1/certificate/CN%3D${1}%2CO%3D${2//[^[:alnum:]]/}%2CC%3DFR/${3}/revocationstatus" \
                | jq .
}

# Révocation d'un certificat
revoke-certificate() {
        curl -X PUT -k -s \
                --cert-type P12 \
                --cert "$ScriptAuthentication" \
                --header "accept: application/json" \
                "https://${AuthorityAddress}/ejbca/ejbca-rest-api/v1/certificate/CN%3D${1}%2CO%3D${2//[^[:alnum:]]/}%2CC%3DFR/${3}/revoke?reason=${4}" \
                | jq .
}

# Création du certificat
create-certificate() {
        # Création du JSON pour la requête API
        Json=$(jq -n \
                --arg ApiCsr "$1" \
                --arg ApiCp "$2" \
                --arg ApiEep "$3" \
                --arg ApiCa "$4" \
                --arg ApiUser "$5" \
                --arg ApiPwd "$6" \
                "$7")

        # Envoi de la requête
        curl -X POST -s -k \
                --cert-type P12 \
                --cert "$ScriptAuthentication" \
                --header 'Content-Type: application/json' \
                --data "$Json" \
                "https://${AuthorityAddress}/ejbca/ejbca-rest-api/v1/certificate/pkcs10enroll" \
                | jq .
}

#===========#
# Arguments #
#===========#

# Initialisation
action="null"
fqdn="null"

case $1 in
        "help")
                display-help
                exit 0
                ;;
        "add"|"new"|"create"|"revoke"|"delete"|"status"|"renew")
                action="$1"
                fqdn="$2"
                reason="$3"
                ;;
        *)
                echo "Erreur : Argument $1 inconnu"
                echo ""
                display-help
                exit 2
                ;;
esac

# Vérification
if [ "$action" == "null" ]; then
        # Aucune action, affichage de l'aide
        display-help
        exit 1
else
        # Vérification du fqdn
        if [ "$fqdn" == "null" ]; then
                echo "$(date '+%F %X %Z') - WARN - un nom de domaine est obligatoire." | tee -a $ManageLog
                echo ""
                display-help
                exit 1
        elif [[ $fqdn =~ ^([a-z0-9]+([a-z0-9]|\-)*\.)+[a-z]{2,} ]]; then
                true
        else
                echo "$(date '+%F %X %Z') - CRIT - le domaine $fqdn n'est pas valide." | tee -a $ManageLog
                exit 2
        fi
fi

# Log
echo "$(date '+%F %X %Z') - INFO - Démarrage (Action: $action | Domaine: $fqdn)" | tee -a $ManageLog

#============#
# Traitement #
#============#

if [ "$action" == "add" -o "$action" == "create" -o "$action" == "new" -o "$action" == "renew" ]; then

        # Vérification de l'état actuel
        if [ "$action" == "renew" ]; then
                # Renouvellement demandé
                if [ ! -d $CertificateRoorDirectory/$fqdn ]; then
                        # Pas de certificat existant, création du dossier et de la clé privée
                        echo "$(date '+%F %X %Z') - WARN - Certificat inexistant. Création du certificat au lieu du renouvellement." | tee -a $ManageLog
                        mkdir -p $CertificateRoorDirectory/$fqdn
                        echo "$(date '+%F %X %Z') - INFO - Création du dossier $CertificateRoorDirectory/$fqdn" | tee -a $ManageLog
                        openssl ecparam -genkey -name prime256v1 -out $CertificateRoorDirectory/$fqdn/private.key
                        echo "$(date '+%F %X %Z') - INFO - Création de la clé privée $CertificateRoorDirectory/$fqdn/private.key" | tee -a $ManageLog
                elif [ ! -f $CertificateRoorDirectory/$fqdn/private.key ]; then
                        # Pas de clé privée à utiliser
                        echo "$(date '+%F %X %Z') - WARN - Clé privée absente. Création d'une nouvelle clé." | tee -a $ManageLog
                        openssl ecparam -genkey -name prime256v1 -out $CertificateRoorDirectory/$fqdn/private.key
                else
                        # Vérification de la clé privée
                        CertificateChecksum=$(openssl x509 -noout -pubkey -in $CertificateRoorDirectory/$fqdn/fullchain.pem 2>&1 | sha256sum | awk '{print $1}')
                        PrivateKeyChecksum=$(openssl pkey -pubout -in $CertificateRoorDirectory/$fqdn/private.key 2>&1 | sha256sum | awk '{print $1}')
                        if [ "$CertificateChecksum" == "$PrivateKeyChecksum" ]; then
                                # Clé valide
                                echo "$(date '+%F %X %Z') - INFO - Clé privée valide." | tee -a $ManageLog
                        else
                                # Clé invalide
                                echo "$(date '+%F %X %Z') - WARN - Clé privée invalide. Création d'une nouvelle clé." | tee -a $ManageLog
                                openssl ecparam -genkey -name prime256v1 -out $CertificateRoorDirectory/$fqdn/private.key
                        fi
                fi
        else
                # Création demandée
                if [ -f $CertificateRoorDirectory/$fqdn/fullchain.pem ]; then
                        # Certificat déjà existant
                        echo "$(date '+%F %X %Z') - WARN - Un certificat pour $fqdn existe déjà." | tee -a $ManageLog
                        echo "$(date '+%F %X %Z') - INFO - Vous pouvez le renouveller à la place sinon il faudra le révoquer au préalable." | tee -a $ManageLog
                        echo "$(date '+%F %X %Z') - INFO - Si vous êtes sûr que ce certificat est déjà révoqué, il est nécessaire de supprimer le dossier $CertificateRoorDirectory/$fqdn." | tee -a $ManageLog
                        exit 1
                elif [ ! -d $CertificateRoorDirectory/$fqdn ]; then
                        # Création du répertoire pour le certificat
                        mkdir -p $CertificateRoorDirectory/$fqdn
                        echo "$(date '+%F %X %Z') - INFO - Création du dossier $CertificateRoorDirectory/$fqdn" | tee -a $ManageLog

                        # Création de la clé privée
                        openssl ecparam -genkey -name prime256v1 -out $CertificateRoorDirectory/$fqdn/private.key
                        echo "$(date '+%F %X %Z') - INFO - Création de la clé privée $CertificateRoorDirectory/$fqdn/private.key" | tee -a $ManageLog
                else
                        # Création de la clé privée
                        openssl ecparam -genkey -name prime256v1 -out $CertificateRoorDirectory/$fqdn/private.key
                        echo "$(date '+%F %X %Z') - INFO - Création de la clé privée $CertificateRoorDirectory/$fqdn/private.key" | tee -a $ManageLog
                fi
        fi

        # Création de la configuration pour openssl
        if [ -e $OpensslConfig  ]; then
                rm -f $OpensslConfig
        fi

        echo "[ req ]" > $OpensslConfig
        echo "default_md = sha256" >> $OpensslConfig
        echo "prompt = no" >> $OpensslConfig
        echo "distinguished_name = dn" >> $OpensslConfig
        echo "" >> $OpensslConfig
        echo "[ dn ]" >> $OpensslConfig
        echo "CN = $fqdn" >> $OpensslConfig
        echo "O = $Organization" >> $OpensslConfig
        echo "C = $Country" >> $OpensslConfig

        # Création de la requête
        CsrContent=$(openssl req -new -key $CertificateRoorDirectory/$fqdn/private.key -config $OpensslConfig)

        # Envoi de la requête
        CreateRequest=$(create-certificate "$CsrContent" "$CertificateProfileName" "$EndEntityProfileName" "$CertificationAuthorityName" "$EnrollmentUser" "$EnrollmentPassword" "$ApiTemplate")

        # Extraction du certificat
        echo $CreateRequest | jq -r .certificate > $CertificateRoorDirectory/$fqdn/cert.b64

        # Conversion du DER en binaire
        openssl base64 -d -A -in $CertificateRoorDirectory/$fqdn/cert.b64 -out $CertificateRoorDirectory/$fqdn/cert.bin

        # Conversion du binaire en PEM
        openssl x509 -inform der -in $CertificateRoorDirectory/$fqdn/cert.bin -out $CertificateRoorDirectory/$fqdn/fullchain.pem

        # Ajout des certificats racines et intermédiaires
        cat $SubCertificate >> $CertificateRoorDirectory/$fqdn/fullchain.pem
        cat $RootCertificate >> $CertificateRoorDirectory/$fqdn/fullchain.pem

        # Log
        echo "$(date '+%F %X %Z') - INFO - Création du certificat $CertificateRoorDirectory/$fqdn/fullchain.pem" | tee -a $ManageLog
        exit 0

elif [ "$action" == "status" ]; then

        # Recherche du n° de série du dernier certificat
        CertificateSerialNumber=$(get-id $fqdn)

        # Demande du status
        CertificateStatusRequest=$(get-status $CertificationAuthorityName $Organization $CertificateSerialNumber)

        # Extraction des données
        CertificateExtractedStatus=$(echo $CertificateStatusRequest | jq .revoked)
        if [ "$CertificateExtractedStatus" == "false" ]; then
                CertificateStatus="actif"
        else
                CertificateStatus="révoqué"
        fi
        CertificateDetails=$(echo $CertificateStatusRequest | jq .revocation_reason)

        # Log
        echo "$(date '+%F %X %Z') - INFO - Certificat $fqdn (S/N: $CertificateSerialNumber) en état : $CertificateStatus (Détails: $CertificateDetails)" | tee -a $ManageLog
        exit 0

elif [ "$action" == "delete" -o "$action" == "revoke" ]; then

        # Validation de la raison de la révocation
        if [ -z "$reason" ]; then
                echo "$(date '+%F %X %Z') - INFO - Aucune raison de révocation indiqué, utilisation de la raison par défaut ($RevokeReason)." | tee -a $ManageLog
        else
                case ${reason^^} in
                        "KEY_COMPROMISE"|"CA_COMPROMISE"|"AFFILIATION_CHANGED"|"SUPERSEDED"|"CESSATION_OF_OPERATION"|"CERTIFICATE_HOLD"|"PRIVILEGES_WITHDRAW"|"AA_COMPROMISE")
                                RevokeReason=${reason^^}
                                echo "$(date '+%F %X %Z') - INFO - Raison de la révocation : $RevokeReason" | tee -a $ManageLog
                                ;;
                        *)
                                echo "$(date '+%F %X %Z') - WARN - Raison de révocation indiquée ($reason) indisponible, utilisation de la raison par défaut ($RevokeReason)." | tee -a $ManageLog
                                ;;
                esac
        fi

        # Recherche du n° de série du dernier certificat
        CertificateSerialNumber=$(get-id $fqdn)

        # Demande du status
        CertificateStatusRequest=$(get-status $CertificationAuthorityName $Organization $CertificateSerialNumber)

        # Extraction des données
        CertificateExtractedStatus=$(echo $CertificateStatusRequest | jq .revoked)

        # Vérification
        if [ "$CertificateExtractedStatus" == "false" ]; then

                # Révocation
                RevokeRequest=$(revoke-certificate $CertificationAuthorityName $Organization $CertificateSerialNumber $RevokeReason)

                # Extraction des données
                RevokeStatus=$(echo $RevokeRequest | jq .revoked)
                RevokeMessage=$(echo $RevokeRequest | jq .message)

                # Log
                if [ "$RevokeStatus" == "true" ]; then
                        /usr/bin/rm -rf $CertificateRoorDirectory/$fqdn
                        echo "$(date '+%F %X %Z') - INFO - Certificat $fqdn révoqué et supprimé (API Message: $RevokeMessage)" | tee -a $ManageLog
                        exit 0
                else
                        echo "$(date '+%F %X %Z') - CRIT - Echec de la révocation pour $fqdn (API Message: $RevokeMessage)" | tee -a $ManageLog
                        echo "$(date '+%F %X %Z') - INFO - Fichiers conservés dans $CertificateRoorDirectory/$fqdn"
                        exit 2
                fi
        else
                # Log
                echo "$(date '+%F %X %Z') - WARN - Certificat $fqdn déjà révoqué)" | tee -a $ManageLog
                exit 1
        fi
else
        # Action inconnue
        echo "$(date '+%F %X %Z') - UNKN - L'action $action n'est pas prévue" | tee -a $ManageLog
        exit 3
fi