From 32aced02d67fd49a5da6463503bf4c22ceec16b9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:12:51 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Fix:=20R=C3=A9solution=20des=20erreurs=20de?= =?UTF-8?q?=20cr=C3=A9ation=20de=20p=C3=A9rim=C3=A8tre=20fonctionnel=20et?= =?UTF-8?q?=20de=20certificats.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ce commit adresse plusieurs problèmes qui empêchaient la création correcte des périmètres fonctionnels et de leurs certificats intermédiaires associés. Modifications apportées : 1. **Scripts Shell (create_root_cert.sh, create_intermediate_cert.sh):** * Correction des chemins de copie pour les fichiers de configuration OpenSSL (`root-openssl.conf`, `intermediate-openssl.conf`) pour utiliser `/opt/scripts/configs/` comme source correcte dans l'environnement d'exécution. * Ajout de `set -e` au début des scripts pour un arrêt immédiat en cas d'erreur. * Modification de `scripts/configs/root-openssl.conf` pour que sa directive `dir` pointe vers `/opt/tls/root`, assurant que la CA Racine trouve correctement ses propres fichiers. * Modification de `scripts/create_intermediate_cert.sh` pour que la directive `dir` dans les fichiers `openssl.cnf` des CA intermédiaires soit dynamiquement ajustée au chemin spécifique du périmètre (ex: `/opt/tls/intermediate/nom_perimetre`). * `create_intermediate_cert.sh` accepte maintenant `ROOT_DOMAIN` comme argument et l'utilise pour rendre uniques les CN des certificats intermédiaires et OCSP (ex: `nom_perimetre.intermediate.example.com`). * Correction des chemins de sortie pour la CRL et les fichiers OCSP (clé privée et CSR) dans `create_intermediate_cert.sh` pour utiliser `$INTERMEDIATE_CA_DIR` comme base. 2. **Contrôleur PHP (PerimeterController.php):** * Extraction dynamique du `ROOT_DOMAIN` à partir du CN du certificat de la CA Racine existante et passage de cette valeur au script `create_intermediate_cert.sh`. * Utilisation d'une expression régulière plus robuste pour l'extraction du `ROOT_DOMAIN`. * Passage des variables d'environnement `OCSP_URL` (depuis la configuration PHP) et `SAN` (vide) au script `create_intermediate_cert.sh` pour éviter les erreurs de "variable has no value" dans OpenSSL. 3. **Initialisation (app/public/index.php):** * Ajout de logs de débogage et d'une gestion d'erreur défensive pour s'assurer que `$_SESSION['init_root_domain']` est correctement défini avant d'être utilisé pour créer la CA Racine. Ces changements combinés résolvent le problème initial de création de certificat intermédiaire et plusieurs autres problèmes découverts, menant à une création réussie des périmètres fonctionnels. --- app/public/index.php | 21 +++++++++ app/src/Controllers/PerimeterController.php | 52 ++++++++++++++++++++- scripts/configs/root-openssl.conf | 2 +- scripts/create_intermediate_cert.sh | 30 ++++++++---- scripts/create_root_cert.sh | 4 ++ 5 files changed, 98 insertions(+), 11 deletions(-) diff --git a/app/public/index.php b/app/public/index.php index 015cce8..ce4a987 100644 --- a/app/public/index.php +++ b/app/public/index.php @@ -81,6 +81,8 @@ if ($userCount === 0 || !$rootCertExists) { if (isset($_POST['admin_password'], $_POST['root_domain']) && !empty($_POST['admin_password']) && !empty($_POST['root_domain'])) { $_SESSION['init_admin_password'] = $_POST['admin_password']; $_SESSION['init_root_domain'] = $_POST['root_domain']; + $logService->log('debug', 'Initialisation - POST data received: admin_password_present=' . !empty($_POST['admin_password']) . ', root_domain=' . ($_POST['root_domain'] ?? 'not_set_or_empty')); + $logService->log('debug', 'Initialisation - Session variables SET: init_admin_password_present=' . !empty($_SESSION['init_admin_password']) . ', init_root_domain=' . ($_SESSION['init_root_domain'] ?? 'not_set_or_empty')); header('Location: ' . $_SERVER['PHP_SELF']); exit(); } else { @@ -108,6 +110,25 @@ if ($userCount === 0 || !$rootCertExists) { if (!$rootCertExists) { echo "

Création du certificat Root CA en cours...

"; $logService->log('info', 'Lancement de la création du certificat Root CA pour le domaine: ' . $_SESSION['init_root_domain'], null, $_SERVER['REMOTE_ADDR']); + + $logService->log('debug', 'Initialisation - About to call create_root_cert.sh. Value of $_SESSION[\'init_root_domain\']: ' . ($_SESSION['init_root_domain'] ?? 'NOT SET OR EMPTY')); + if (empty($_SESSION['init_root_domain'])) { + $logService->log('error', 'Initialisation - CRITICAL: $_SESSION[\'init_root_domain\'] is empty or not set right before calling create_root_cert.sh. Forcing display of error and form again.'); + // Code to re-display form or a clear error message, then exit. + // This is to prevent the script from being called with an empty argument. + echo "

Erreur Critique: La variable de session pour le domaine racine est vide avant d'appeler le script de création. Veuillez réessayer.

"; + // Minimal form for resubmission: + echo "
"; + echo "
"; + echo "
Exemple: exemple.com
"; + echo ""; + echo "
"; + // Optionally, unset session variables to force re-entry of CAS 1.2 logic fully. + unset($_SESSION['init_admin_password']); + unset($_SESSION['init_root_domain']); + exit(); + } + // Exécution du script shell de création de certificat root avec le domaine racine $command = escapeshellcmd(SCRIPTS_PATH . '/create_root_cert.sh ' . escapeshellarg($_SESSION['init_root_domain'])); $output = shell_exec($command . ' 2>&1'); diff --git a/app/src/Controllers/PerimeterController.php b/app/src/Controllers/PerimeterController.php index 6b1e7ce..25be799 100644 --- a/app/src/Controllers/PerimeterController.php +++ b/app/src/Controllers/PerimeterController.php @@ -89,6 +89,10 @@ class PerimeterController $perimeterName = trim($_POST['name'] ?? ''); $ipAddress = $_SERVER['REMOTE_ADDR']; $userId = $this->authService->getUserId(); + // La passphrase est optionnelle pour l'intermédiaire, mais le script attend l'argument. + // Le script create_intermediate_cert.sh a été modifié pour accepter "EMPTY_STRING" + // si aucune passphrase n'est fournie. + $passphrase = trim($_POST['intermediate_passphrase'] ?? ''); // Assumons que cela vient du formulaire if (empty($perimeterName)) { $_SESSION['error'] = $this->langService->__('perimeter_create_error_empty_name'); @@ -105,14 +109,58 @@ class PerimeterController exit(); } + // Extraire ROOT_DOMAIN du certificat CA racine + $rootCaCertPath = ROOT_CA_PATH . '/certs/ca.cert.pem'; + $rootDomain = null; + if (!file_exists($rootCaCertPath)) { + $this->logService->log('error', "Certificat CA racine non trouvé à: $rootCaCertPath", $userId, $ipAddress); + $_SESSION['error'] = $this->langService->__('perimeter_create_error_root_cert_missing'); // Nouvelle clé de traduction + header('Location: /perimeters/create'); + exit(); + } + + $subjectCommand = "openssl x509 -noout -subject -in " . escapeshellarg($rootCaCertPath); + $subjectLine = shell_exec($subjectCommand); + + if ($subjectLine && preg_match('/CN\s*=\s*ca\.([^\/,\s]+)/', $subjectLine, $matches)) { + $rootDomain = $matches[1]; + } + + if (empty($rootDomain)) { + $this->logService->log('error', "Impossible d'extraire ROOT_DOMAIN du certificat CA racine. Regex: '/CN\s*=\s*ca\.([^\/,\s]+)/' Sujet obtenu: " . ($subjectLine ?: 'vide ou non recupere'), $userId, $ipAddress); + $_SESSION['error'] = $this->langService->__('perimeter_create_error_root_domain'); // Clé de traduction existante ou à ajouter + header('Location: /perimeters/create'); + exit(); + } + $this->logService->log('info', "ROOT_DOMAIN extrait avec succès: $rootDomain", $userId, $ipAddress); + // Appeler le script shell pour créer le certificat intermédiaire - $command = escapeshellcmd(SCRIPTS_PATH . '/create_intermediate_cert.sh') . ' ' . escapeshellarg($perimeterName); + // Retrieve OCSP_URL from defined constant + $ocspUrl = OCSP_URL; // This constant is defined in app/src/config/app.php + $sanValue = ''; // SAN is not typically needed for an intermediate CA, set to empty + + $passphraseArg = !empty($passphrase) ? $passphrase : "EMPTY_STRING"; + + // Construct the shell command with environment variables + // Note: escapeshellcmd is applied to the script path. + // Environment variable values should also be safe. + // Using escapeshellarg for values assigned to env vars is a good practice. + $scriptPath = SCRIPTS_PATH . '/create_intermediate_cert.sh'; + $command = "OCSP_URL=" . escapeshellarg($ocspUrl) . " SAN=" . escapeshellarg($sanValue) . " " . + escapeshellcmd($scriptPath) . ' ' . + escapeshellarg($perimeterName) . ' ' . + escapeshellarg($passphraseArg) . ' ' . + escapeshellarg($rootDomain); $this->logService->log('info', "Tentative de création du périmètre '$perimeterName' et de son certificat intermédiaire. Commande: '$command'", $userId, $ipAddress); $output = shell_exec($command . ' 2>&1'); - if (strpos($output, "Certificat Intermédiaire CA pour '$perimeterName' créé avec succès") !== false) { + // La condition de succès doit correspondre à la sortie du script. + // Le script create_intermediate_cert.sh se termine par : + // echo "Certificat Intermédiaire CA pour '$FUNCTIONAL_PERIMETER_NAME' créé : $INTERMEDIATE_CERT" + // Nous allons donc chercher une partie de cette chaîne. + if (strpos($output, "Certificat Intermédiaire CA pour '$perimeterName' créé") !== false) { // Enregistrer le périmètre dans la base de données $stmt = $this->db->prepare("INSERT INTO functional_perimeters (name, intermediate_cert_name) VALUES (?, ?)"); $intermediateCertFileName = "intermediate.cert.pem"; // Nom générique du fichier pour l'intermédiaire diff --git a/scripts/configs/root-openssl.conf b/scripts/configs/root-openssl.conf index 6e53243..699d8cd 100644 --- a/scripts/configs/root-openssl.conf +++ b/scripts/configs/root-openssl.conf @@ -2,7 +2,7 @@ default_ca = CA_default # The default ca section [ CA_default ] -dir = /opt/tls # Where everything is kept +dir = /opt/tls/root # Where everything is kept for the Root CA certs = $dir/certs # Where the issued certs are kept database = $dir/index.txt # database index file. # several certs with same subject. diff --git a/scripts/create_intermediate_cert.sh b/scripts/create_intermediate_cert.sh index 8141781..62536b5 100644 --- a/scripts/create_intermediate_cert.sh +++ b/scripts/create_intermediate_cert.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e # Ce script crée un certificat CA intermédiaire signé par le Root CA. # Il est appelé par l'application PHP lors de la création d'un nouveau "périmètre fonctionnel". @@ -6,14 +7,21 @@ # Arguments : # $1: Nom du périmètre fonctionnel (utilisé comme nom du dossier et dans le CN du certificat) # $2: (Optionnel) Phrase secrète pour la clé privée de l'intermédiaire +# $3: Domaine racine (ex: exemple.com) FUNCTIONAL_PERIMETER_NAME="$1" -INTERMEDIATE_KEY_PASSPHRASE="$2" # Optionnel +INTERMEDIATE_KEY_PASSPHRASE="$2" # Optional, can be empty string if no passphrase +ROOT_DOMAIN="$3" -if [ -z "$FUNCTIONAL_PERIMETER_NAME" ]; then - echo "Usage: $0 [key_passphrase]" +if [ -z "$FUNCTIONAL_PERIMETER_NAME" ] || [ -z "$ROOT_DOMAIN" ]; then + echo "Usage: $0 " + echo "Error: Functional perimeter name and root domain are required." exit 1 fi +# If INTERMEDIATE_KEY_PASSPHRASE is the literal "EMPTY_STRING", set it to an actual empty string. +if [ "$INTERMEDIATE_KEY_PASSPHRASE" == "EMPTY_STRING" ]; then + INTERMEDIATE_KEY_PASSPHRASE="" +fi ROOT_CA_DIR="/opt/tls/root" INTERMEDIATE_CA_DIR="/opt/tls/intermediate/$FUNCTIONAL_PERIMETER_NAME" @@ -32,6 +40,12 @@ echo "Démarrage de la création du certificat Intermédiaire pour '$FUNCTIONAL_ # Créer les dossiers nécessaires pour la PKI Intermédiaire mkdir -p "$INTERMEDIATE_CA_DIR/certs" "$INTERMEDIATE_CA_DIR/crl" "$INTERMEDIATE_CA_DIR/newcerts" "$INTERMEDIATE_CA_DIR/private" "$INTERMEDIATE_CA_DIR/csr" +# Copier le fichier de configuration OpenSSL pour l'intermédiaire +cp /opt/scripts/configs/intermediate-openssl.conf "$INTERMEDIATE_CNF" + +# Adjust the 'dir' variable in the copied OpenSSL config to point to the specific intermediate CA directory +sed -i "s|^dir\s*=\s*/opt/tls/intermediate.*|dir = $INTERMEDIATE_CA_DIR|" "$INTERMEDIATE_CNF" + # Initialiser les fichiers requis par OpenSSL pour une CA intermédiaire chmod 700 "$INTERMEDIATE_CA_DIR/private" touch "$INTERMEDIATE_CA_DIR/index.txt" @@ -55,7 +69,7 @@ chmod 400 "$INTERMEDIATE_KEY" openssl req -new -sha256 \ -key "$INTERMEDIATE_KEY" $([ -n "$INTERMEDIATE_KEY_PASSPHRASE" ] && echo "-passin pass:\"$INTERMEDIATE_KEY_PASSPHRASE\"") \ -out "$INTERMEDIATE_CSR" \ - -subj "/C=FR/ST=NORD/L=ROUBAIX/O=IT/OU=IT/emailAddress=sec@tips-mine.com/CN=intermediate-cert.$ROOT_DOMAIN/" \ + -subj "/C=FR/ST=NORD/L=ROUBAIX/O=IT/OU=IT/emailAddress=sec@tips-mine.com/CN=$FUNCTIONAL_PERIMETER_NAME.intermediate.$ROOT_DOMAIN/" \ -config "$INTERMEDIATE_CNF" # Utilise le CNF de l'intermédiaire pour la création de la CSR # Signer la CSR de l'Intermédiaire avec le Root CA @@ -70,16 +84,16 @@ cat "$INTERMEDIATE_CERT" "$ROOT_CERT" > "$INTERMEDIATE_CHAIN" chmod 444 "$INTERMEDIATE_CHAIN" # Create a Certificate revocation list of the intermediate CA -openssl ca -config "$INTERMEDIATE_CNF" -gencrl -out "$INTERMEDIATE_CNF/crl/intermediate.crl.pem" +openssl ca -config "$INTERMEDIATE_CNF" -gencrl -out "$INTERMEDIATE_CA_DIR/crl/intermediate.crl.pem" # Create OSCP key pair -openssl genrsa -out "$INTERMEDIATE_CNF/private/ocsp.key.pem" 4096 +openssl genrsa -out "$INTERMEDIATE_CA_DIR/private/ocsp.key.pem" 4096 # Create the OSCP CSR openssl req -new -sha256 \ -key "$INTERMEDIATE_CA_DIR/private/ocsp.key.pem" \ - -out "$INTERMEDIATE_CNF/crl/intermediate.crl.pem" -nodes \ - -subj "/C=FR/ST=NORD/L=ROUBAIX/O=IT/OU=IT/emailAddress=sec@tips-mine.com/CN=ocsp-cert.$ROOT_DOMAIN/"\ + -out "$INTERMEDIATE_CA_DIR/csr/ocsp.csr.pem" -nodes \ + -subj "/C=FR/ST=NORD/L=ROUBAIX/O=IT/OU=IT/emailAddress=sec@tips-mine.com/CN=$FUNCTIONAL_PERIMETER_NAME.ocsp.$ROOT_DOMAIN/"\ -config "$INTERMEDIATE_CNF" echo "Certificat Intermédiaire CA pour '$FUNCTIONAL_PERIMETER_NAME' créé : $INTERMEDIATE_CERT" diff --git a/scripts/create_root_cert.sh b/scripts/create_root_cert.sh index cd06991..2620a24 100644 --- a/scripts/create_root_cert.sh +++ b/scripts/create_root_cert.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e # Ce script crée le certificat Root CA (Certificate Authority) auto-signé. # Il est destiné à être exécuté une seule fois, au premier lancement de l'application. @@ -25,6 +26,9 @@ mkdir -p "$ROOT_CA_DIR/certs" "$ROOT_CA_DIR/crl" "$ROOT_CA_DIR/newcerts" "$ROOT_ chmod 777 "$ROOT_CA_DIR/certs" "$ROOT_CA_DIR/crl" "$ROOT_CA_DIR/newcerts" "$ROOT_CA_DIR/private" "$ROOT_CA_DIR/csr" +# Copier le fichier de configuration OpenSSL pour la CA Racine +cp /opt/scripts/configs/root-openssl.conf "$ROOT_CNF" + # Initialiser les fichiers requis par OpenSSL pour une CA touch "$ROOT_CA_DIR/index.txt" From fc3a86a839ebbb6777ab7d91291cc03a6ebc09d5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:41:19 +0000 Subject: [PATCH 2/2] Fix: Ensure successful creation of leaf certificates. This commit addresses several issues related to the creation of leaf certificates signed by functional scope intermediate CAs: 1. **Providing environment variables to `create_cert.sh`:** * `CertificateController.php` was modified to extract the `ROOT_DOMAIN` from the Root CA certificate. * The `SAN` (Subject Alternative Name) environment variable is now built dynamically (e.g., `DNS:cert_name.scope_name.root_domain`). * The `OCSP_URL` (from the application configuration) and `SAN` environment variables are passed to the execution environment of the `create_cert.sh` script. This resolves "variable has no value" errors in OpenSSL when using the intermediate CA configuration file that references `$ENV::OCSP_URL` and `$ENV::SAN`. 2. **Explicit use of `v3_leaf` extensions:** * The `scripts/create_cert.sh` script was modified to explicitly use `-reqexts v3_leaf` (for the CSR) and `-extensions v3_leaf` (for CA signing). This ensures that the `[v3_leaf]` section of the OpenSSL configuration file (containing SAN and OCSP directives) is correctly applied to leaf certificates. 3. **Correction of success detection in `CertificateController.php`:** * The string searched for by `strpos` to detect a success message from `create_cert.sh` has been made more precise, ensuring that the PHP controller correctly interprets the script's result. These changes ensure that leaf certificates are created with the correct extensions and that their creation is properly recognized by the application. --- app/src/Controllers/CertificateController.php | 42 ++++++++++++++++--- scripts/create_cert.sh | 4 +- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/app/src/Controllers/CertificateController.php b/app/src/Controllers/CertificateController.php index bd1d6d6..0494056 100644 --- a/app/src/Controllers/CertificateController.php +++ b/app/src/Controllers/CertificateController.php @@ -133,9 +133,38 @@ class CertificateController } $functionalPerimeterName = $perimeter['name']; - // Préparer la commande du script shell - // Important: utiliser escapeshellarg pour protéger les arguments - $command = escapeshellcmd(SCRIPTS_PATH . '/create_cert.sh') . ' ' . + // Extraire ROOT_DOMAIN du certificat CA racine pour SAN et OCSP + $rootCaCertPath = ROOT_CA_PATH . '/certs/ca.cert.pem'; + if (!file_exists($rootCaCertPath)) { + $this->logService->log('error', "Certificat CA racine non trouvé pour extraction ROOT_DOMAIN lors de la création de cert feuille.", $userId, $ipAddress); + $_SESSION['error'] = $this->langService->__('cert_create_error_root_cert_missing'); // Needs this translation key + header('Location: /certificates/create'); + exit(); + } + $subjectCommand = "openssl x509 -noout -subject -in " . escapeshellarg($rootCaCertPath); + $subjectLine = shell_exec($subjectCommand); + $rootDomain = null; + if ($subjectLine && preg_match('/CN\s*=\s*ca\.([^\/,\s]+)/', $subjectLine, $matches)) { + $rootDomain = $matches[1]; + } + if (empty($rootDomain)) { + $this->logService->log('error', "Impossible d'extraire ROOT_DOMAIN du cert CA racine (pour SAN). Regex: '/CN\s*=\s*ca\.([^\/,\s]+)/' Sujet: " . ($subjectLine ?: 'vide'), $userId, $ipAddress); + $_SESSION['error'] = $this->langService->__('cert_create_error_root_domain_extraction'); // Needs this translation key + header('Location: /certificates/create'); + exit(); + } + $this->logService->log('info', "ROOT_DOMAIN extrait pour SAN/OCSP: $rootDomain", $userId, $ipAddress); + + // Construire la valeur SAN + $sanValue = "DNS:" . $subdomainName . "." . $functionalPerimeterName . "." . $rootDomain; + + // Récupérer OCSP_URL + $ocspUrl = OCSP_URL; // Constante de app/src/config/app.php + + // Préparer la commande du script shell avec les variables d'environnement + $scriptPath = SCRIPTS_PATH . '/create_cert.sh'; + $command = "OCSP_URL=" . escapeshellarg($ocspUrl) . " SAN=" . escapeshellarg($sanValue) . " " . + escapeshellcmd($scriptPath) . ' ' . escapeshellarg($subdomainName) . ' ' . escapeshellarg($functionalPerimeterName); @@ -144,8 +173,11 @@ class CertificateController // Exécuter le script shell $output = shell_exec($command . ' 2>&1'); // Redirige stderr vers stdout - // Vérifier le résultat du script (simple vérification de chaîne, une meilleure parsage serait utile) - if (strpos($output, "Certificat '${subdomainName}.${functionalPerimeterName}.cert' créé avec succès") !== false) { + // Vérifier le résultat du script + // Le script create_cert.sh sort "Certificat '$CERT_BASE_NAME' créé avec succès : $CERT_FILE" + // où $CERT_BASE_NAME est "${SUBDOMAIN_OR_CN_NAME}.${FUNCTIONAL_PERIMETER_NAME}" + $certBaseNameForCheck = $subdomainName . '.' . $functionalPerimeterName; + if (strpos($output, "Certificat '" . $certBaseNameForCheck . "' créé avec succès :") !== false) { // Extraire la date d'expiration du certificat créé (en lisant le fichier cert ou en estimant 1 an) $certFileName = "{$subdomainName}.{$functionalPerimeterName}.cert.pem"; $fullCertPath = INTERMEDIATE_CA_PATH_BASE . "/{$functionalPerimeterName}/certs/{$certFileName}"; diff --git a/scripts/create_cert.sh b/scripts/create_cert.sh index 0abdbc4..1263b56 100644 --- a/scripts/create_cert.sh +++ b/scripts/create_cert.sh @@ -42,10 +42,10 @@ chmod 400 "$KEY_FILE" # Permissions strictes # Le Common Name (CN) est important pour les certificats SSL/TLS openssl req -new -sha256 -key "$KEY_FILE" -out "$CSR_FILE" \ -subj "/C=FR/ST=Hauts-de-France/L=Roubaix/O=GestionCertif/OU=${FUNCTIONAL_PERIMETER_NAME}/CN=${SUBDOMAIN_OR_CN_NAME}.cert-gestion.local" \ - -reqexts usr_cert -config "$INTERMEDIATE_CNF" # Utilise le CNF de l'intermédiaire et ses extensions usr_cert + -reqexts v3_leaf -config "$INTERMEDIATE_CNF" # Utilise le CNF de l'intermédiaire et ses extensions v3_leaf # Signer la CSR avec le CA intermédiaire -openssl ca -batch -config "$INTERMEDIATE_CNF" -extensions usr_cert -days 365 -notext -md sha256 \ +openssl ca -batch -config "$INTERMEDIATE_CNF" -extensions v3_leaf -days 365 -notext -md sha256 \ -in "$CSR_FILE" \ -out "$CERT_FILE"