Merge pull request #10 from tips-of-mine/fix/functional-perimeter-creation

Fix/functional perimeter creation
This commit is contained in:
tips-of-mine
2025-06-15 19:41:34 +02:00
committed by GitHub
7 changed files with 137 additions and 18 deletions

View File

@ -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 "<p>Création du certificat Root CA en cours...</p>";
$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 "<p style='color: red;'><strong>Erreur Critique:</strong> La variable de session pour le domaine racine est vide avant d'appeler le script de création. Veuillez réessayer.</p>";
// Minimal form for resubmission:
echo "<form method='POST' action='" . htmlspecialchars($_SERVER['PHP_SELF']) . "'>";
echo "<div><label for='admin_password'>Mot de passe administrateur initial :</label><input type='password' id='admin_password' name='admin_password' required></div>";
echo "<div><label for='root_domain'>Domaine racine (ROOT_DOMAIN) pour le certificat CA :</label><input type='text' id='root_domain' name='root_domain' required><small>Exemple: exemple.com</small></div>";
echo "<button type='submit'>Configurer</button>";
echo "</form>";
// 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');

View File

@ -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}";

View File

@ -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

View File

@ -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.

View File

@ -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"

View File

@ -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 <functional_perimeter_name> [key_passphrase]"
if [ -z "$FUNCTIONAL_PERIMETER_NAME" ] || [ -z "$ROOT_DOMAIN" ]; then
echo "Usage: $0 <functional_perimeter_name> <key_passphrase|EMPTY_STRING> <root_domain>"
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"

View File

@ -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"