EJBCA - Création de certificats TLS
Création — Nicolas 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
- Sur l'interface d'administration, dans la section
CA Functions, on va dansCertificate Profileset on clone le profil par défautSERVER:

- On nomme le modèle et on le créé en cliquant sur
Create from template:

- On édite le modèle fraichement créé via son bouton
Edit:

- On commence par définir :
- Available Key Algorithms :
ECDSA - Available ECDSA curves :
P-256 / prime256v1 / secp256r1 - Signature Algorithm :
Inherit from Issuing CA - Validity or end date of the certificate :
1y - Expriration Restriction : On coche la cae
Use…puis dans la section suivante, on coche les cases :- Monday
- Friday
- Saturday
- Sunday
- Profile Description : Optionnel, permet de mieux identifier le modèle.

- Dans la section
X.509v3 extensions, on décoche :- Basic Constraints

- Dans la section
X.509v3 extensions - Usages, on coche :- Key Usage
- Digital Signature
- Key encipherment
- Dans Extended Key Usage, on choisira :
- Server Authentication

- Dans la section
X.509v3 extensions - Names, :- on coche Subject Alternative Name
- on décoche Issuer Alternative Name

- Dans la section
X.509v3 extensions - Validation data, on coche :- CRL Distribution Points
- Use CA defined CRL Distribution Point
- Authority Information Access :
- Use CA defined OCSP locator
- Use CA defined CA issuer

- Dans la section
Other Data:- on décoche LDAP DN order
- dans la partie Available CAs, on sélectionne notre autorité intermédiaire.
- on sauvegarde le tout en cliquant sur
Save
Création du modèle de serveur
- On va désormais dans la menu
RA Functions,End Entity Profileset dans la sectionAdd End Entity Profile, on nomme notre modèle et on l'ajoute ajoute en cliquant surAdd Profile:

- On sélectionne notre nouveau profil dans la liste et on l'édite :

- On commence par décocher End Entity E-mail, on peut aussi ajouter si on le souhaite une description :

- Dans la section
Subject DN Attributes, on sélectionnes dans le menu déroulant et on ajoute à la suite deCN, Common namedéjà présent avec le boutonAdd:- O, Organization : on indiquera le nom de notre organisation et on cochera
Requireduniquement - C, Country (ISO 3166) : on indiquera le code du pays de l'organisation et on cochera
Requireduniquement
- Pour CN, Common name, on cochera
RequiredetModifiable

- Dans la section
Other Subject Attributes, pour l'attributSubject Alternative Name, on ajouteraDNS Nameet on cocheraUse entity CN field:

- Dans la section
Main Certificate Data, on choisira :- pour Default Certificate Profile : le profil TLS précédemment créé
- pour Default CAs : l'autorité intermédiaire précédemment créée
- pour Default Token :
User Generated - pour Available Tokens :
User GeneratedetPEM file
- Puis on sauvegarde le tout avec le bouton
Saveen bas de la page :

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
- On commence en ligne de commande par créer une configuration pour openssl :
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>
- On crée un clé privée :
openssl ecparam -genkey -name prime256v1 -out <fqdn>.key
- On crée une requête de certification :
openssl req -new -key <fqdn>.key -config /tmp/openssl.conf
- On copie cette requête et on va sur l'interface de gestion, soit via l'interface d'administration en cliquant sur le menu
RA Web(tout en bas des menus), soit directement via l'adressehttps://<ejbca-address>/ejbca/ra. - Une fois sur l'interface, on clique sur
Make New Request:

- On sélectionne :
- Notre profil de certificat TLS
- Notre profil de certification pour du TLS
- On choisit
Provided by user - On colle notre requête créée en ligne commande dans
Upload CSR - On clique sur
Upload CSR

- On trouve alors le résumé de notre requête. On ajoutera dans le champs
Usernamede la sectionProvide User Credentials, leFQDNprincipale pour ce certificat puis on pourra le télécharger en cliquant sur un des boutons de téléchargement situé en bas de page, chacun correspondant à un type précis de certificat (selon les besoin de votre serveur web donc) :
Par appel API
- Pour faciliter la création/interrogation/révocation par appel API, j'ai créé un script
manage-certificate.sh:
#!/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
- Il suffit de modifier les variables :
WorkingDirectory: Le dossier dans lesquels sont stockés les certificats créés, le certificat du script, etc…AuthorityAddress: L'adresse de votre autorité de certification.CertificateProfileName: Le nom de votre profil de certificat.EndEntityProfileName: Le nom de votre profil de serveur.CertificationAuthorityName: Le nom de votre autorité de certification.EnrollmentUser: Un nom de compte à associer aux certificats créés. Si il n'existe pas, il sera créé automatiquement.EnrollmentPassword: Le mot de passe du compte précédent.Organization: Le nom de votre organisation.Country: Votre pays.
- On place le certificat du script dans
$WorkingDirectoryet les certificats racine et intermédiaire au formatPEMdans$WorkingDirectory/issued/. - Il ne reste plus qu'à lancer le script.
Discussion