diff --git a/app/public/index.php b/app/public/index.php index fa34085..30c2eee 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 ""; + // 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/CertificateController.php b/app/src/Controllers/CertificateController.php index f1990e6..b92b5e9 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/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_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" 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