Audit Sécurité PKIaaS - Stockage des Clés et Mots de Passe
Résumé Exécutif
🔴 Vulnérabilités Critiques Identifiées
- Stockage des clés privées : Usage de
Crypt::encryptString()Laravel (clé unique système) - Mots de passe d'approbation : Hashage correct mais pas de salt explicite personnalisé
- Challenge passwords SCEP : Stockage en clair dans la base de données
- Architecture Nitrokey : Design initial incompatible avec clés physiques distantes
📊 Score de Sécurité Global : 3/10
1. Analyse Détaillée des Vulnérabilités
🔴 CRITIQUE - Stockage des Clés Privées
Problèmes Identifiés
// app/Models/Certificate.php:79-84
public function getDecryptedPrivateKeyAttribute()
{
return Crypt::decryptString($this->private_key); // ❌ Clé unique système
}
public function setPrivateKeyAttribute($value)
{
$this->attributes['private_key'] = Crypt::encryptString($value); // ❌ Pas de salt/dérivation
}
// app/Models/CertificateAuthority.php:104-109 - MÊME PROBLÈME
Impact Sécurité
- Une seule clé de chiffrement pour toutes les clés privées (APP_KEY)
- Pas de dérivation de clé basée sur des éléments uniques
- Compromission globale si APP_KEY est exposée
- Non-conformité FIPS 140-2 (pas de HSM, pas de séparation)
Recommandations
// Nouveau système avec dérivation de clé unique par CA/certificat
class SecureKeyStorage
{
public function encryptPrivateKey(string $privateKey, string $caId, string $salt = null): string
{
$salt = $salt ?? random_bytes(32);
$derivedKey = $this->deriveKey($caId, $salt);
// Utiliser sodium_crypto_secretbox pour chiffrement authentifié
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$encrypted = sodium_crypto_secretbox($privateKey, $nonce, $derivedKey);
return base64_encode($salt . $nonce . $encrypted);
}
private function deriveKey(string $caId, string $salt): string
{
return sodium_crypto_pwhash(
SODIUM_CRYPTO_SECRETBOX_KEYBYTES,
config('app.key') . $caId, // Base + CA unique
$salt,
SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
);
}
}
🟡 MOYEN - Challenge Passwords SCEP
Problème Identifié
// database/migrations/2025_09_16_041048 - Ligne 19
$table->string('scep_challenge_password')->nullable()
->comment('SCEP challenge password for enrollment'); // ❌ En clair !
Impact
- Mots de passe en clair dans la base de données
- Accès administrateur = compromission de tous les challenge passwords
- Logs potentiels exposant les mots de passe
Recommandation
// Nouveau champ avec hash + salt
$table->string('scep_challenge_password_hash')->nullable()
->comment('Hashed SCEP challenge password with salt');
$table->string('scep_challenge_salt')->nullable();
// Dans le modèle
public function setSCEPChallengePassword(string $password): void
{
$salt = random_bytes(32);
$this->scep_challenge_salt = base64_encode($salt);
$this->scep_challenge_password_hash = hash_pbkdf2('sha256', $password, $salt, 100000);
}
🟡 MOYEN - Architecture Nitrokey Inappropriée
Problèmes de Design Initial
// app/Services/Crypto/NitrokeyCryptoService.php (conceptuel)
private function retrieveShare(string $nitrokeyId, int $caId): array
{
// ❌ Assume que Nitrokey est connectée AU SERVEUR
$session = $this->pkcs11Connect($nitrokeyId);
}
Contraintes Réelles
- Nitrokeys sur postes clients distants (pas sur serveur)
- Accès PKCS#11 distant requis via réseau
- Protocole sécurisé pour transmission des parts de clés
- Interface web pour orchestrer la génération/utilisation
2. Architecture Nitrokey Corrigée
🏗️ Nouvelle Architecture Distribuée
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Client 1 │ │ PKIaaS Server │ │ Client N │
│ Nitrokey NK001 │◄──►│ │◄──►│ Nitrokey NK00N │
│ PKCS#11 Local │ │ Orchestrateur │ │ PKCS#11 Local │
└─────────────────┘ │ Shamir Reconst. │ └─────────────────┘
└──────────────────┘
Composants Requis
1. Agent Client Nitrokey
// ClientAgent installé sur chaque poste avec Nitrokey
class NitrokeyClientAgent
{
private $pkcs11Session;
private $serverEndpoint;
public function participateInSigning(string $sessionId): bool
{
// 1. Connecter à la Nitrokey locale via OpenSC/PKCS#11
$this->pkcs11Session = $this->connectLocalNitrokey();
// 2. Demander PIN utilisateur (interface graphique)
$pin = $this->promptUserForPIN();
// 3. Déverrouiller Nitrokey avec PIN
$this->authenticateWithPIN($pin);
// 4. Récupérer la part Shamir stockée
$shamirShare = $this->retrieveLocalShamirShare($sessionId);
// 5. Chiffrer avec clé session et transmettre au serveur
return $this->transmitEncryptedShare($sessionId, $shamirShare);
}
private function connectLocalNitrokey(): PKCS11Session
{
// Utiliser OpenSC PKCS#11 library
$pkcs11 = new PKCS11('/usr/lib/opensc-pkcs11.so');
return $pkcs11->openSession();
}
}
2. Interface Web de Génération
// Interface pour générer les parts Shamir depuis l'admin web
class ShamirWebInterface extends Controller
{
public function generateDistributedCA(Request $request)
{
$validated = $request->validate([
'ca_name' => 'required|string',
'threshold' => 'required|integer|min:2',
'total_shares' => 'required|integer|min:3',
'nitrokey_assignments' => 'required|array' // NK001 -> user@domain.com
]);
// 1. Générer CA private key maître
$masterPrivateKey = $this->generateCAMasterKey(4096);
// 2. Appliquer Shamir Secret Sharing
$shamirShares = $this->applyShamirSplitting(
$masterPrivateKey,
$validated['threshold'],
$validated['total_shares']
);
// 3. Créer "deployment packages" pour chaque Nitrokey
$deploymentPackages = [];
foreach ($shamirShares as $index => $share) {
$nitrokeyId = "NK" . str_pad($index, 3, '0', STR_PAD_LEFT);
$deploymentPackages[$nitrokeyId] = [
'share_data' => $this->encryptShareForDeployment($share),
'deployment_instructions' => $this->generateDeploymentScript($nitrokeyId),
'assigned_user' => $validated['nitrokey_assignments'][$nitrokeyId],
'qr_code' => $this->generateDeploymentQR($nitrokeyId, $share)
];
}
// 4. Effacement sécurisé de la clé maître
sodium_memzero($masterPrivateKey);
// 5. Retourner packages de déploiement
return response()->json([
'ca_id' => $caId,
'deployment_packages' => $deploymentPackages,
'next_steps' => 'Distribute packages to Nitrokey holders'
]);
}
}
3. Protocole de Signature Distribuée
class DistributedSigningOrchestrator
{
public function initiateSigning(string $csr, int $caId): string
{
// 1. Créer session de signature temporaire
$sessionId = $this->createSigningSession($caId, $csr);
// 2. Identifier les Nitrokeys requises
$requiredNitrokeys = $this->getRequiredNitrokeys($caId);
// 3. Notifier les détenteurs de Nitrokeys
foreach ($requiredNitrokeys as $nitrokey) {
$this->notifyNitrokeyHolder($nitrokey, $sessionId);
}
// 4. Attendre les contributions (timeout 5 minutes)
return $this->waitForSigningContributions($sessionId);
}
public function receiveNitrokeyContribution(string $sessionId, string $nitrokeyId, array $encryptedShare): bool
{
// 1. Valider la session active
$session = $this->getSigningSession($sessionId);
if (!$session || $session['status'] !== 'waiting_contributions') {
throw new InvalidSigningSessionException();
}
// 2. Vérifier autorisation de cette Nitrokey
if (!in_array($nitrokeyId, $session['required_nitrokeys'])) {
throw new UnauthorizedNitrokeyException();
}
// 3. Déchiffrer et stocker la part
$shamirShare = $this->decryptShareContribution($encryptedShare, $session['session_key']);
$session['received_shares'][$nitrokeyId] = $shamirShare;
// 4. Vérifier si seuil atteint
if (count($session['received_shares']) >= $session['threshold']) {
return $this->executeSigningWithShares($sessionId);
}
return true; // En attente d'autres contributions
}
private function executeSigningWithShares(string $sessionId): bool
{
$session = $this->getSigningSession($sessionId);
// 1. Reconstituer clé privée temporairement
$reconstructedPrivateKey = $this->shamirReconstruct($session['received_shares']);
// 2. Signer le CSR
$signedCertificate = $this->signCSRWithReconstructedKey(
$session['csr'],
$reconstructedPrivateKey,
$session['ca_id']
);
// 3. Effacement immédiat et sécurisé
sodium_memzero($reconstructedPrivateKey);
foreach ($session['received_shares'] as &$share) {
sodium_memzero($share);
}
// 4. Nettoyage session
$this->cleanupSigningSession($sessionId);
// 5. Retourner certificat signé
return $signedCertificate;
}
}
🔧 Intégration OpenSC/PKCS#11
Prérequis Système
# Installation OpenSC sur les postes clients
apt-get install opensc opensc-pkcs11
# ou
yum install opensc pcsc-lite-devel
# Vérification Nitrokey HSM détectée
pkcs11-tool --module /usr/lib/opensc-pkcs11.so --list-slots
# Test accès avec PIN
pkcs11-tool --module /usr/lib/opensc-pkcs11.so --list-objects --login --pin 123456
Interface PHP PKCS#11
// Extension PHP PKCS#11 ou wrapper via CLI
class PKCS11Wrapper
{
private $moduleLib;
private $slotId;
public function __construct(string $moduleLib = '/usr/lib/opensc-pkcs11.so')
{
$this->moduleLib = $moduleLib;
$this->detectNitrokey();
}
public function storeObject(array $objectData, string $pin): bool
{
$cmd = sprintf(
'pkcs11-tool --module %s --login --pin %s --write-object %s --type data --id %s --label "%s"',
escapeshellarg($this->moduleLib),
escapeshellarg($pin),
escapeshellarg($objectData['data']),
escapeshellarg($objectData['id']),
escapeshellarg($objectData['label'])
);
$result = shell_exec($cmd . ' 2>&1');
return strpos($result, 'OK') !== false;
}
public function retrieveObject(string $objectId, string $pin): ?string
{
$cmd = sprintf(
'pkcs11-tool --module %s --login --pin %s --read-object --id %s --type data',
escapeshellarg($this->moduleLib),
escapeshellarg($pin),
escapeshellarg($objectId)
);
return shell_exec($cmd);
}
}
3. Plan de Correction Prioritaire
🚨 Phase 1 - Correctifs Critiques (1 semaine)
1.1 Sécurisation Stockage Clés Privées
// Nouveau service de stockage sécurisé
php artisan make:service SecureKeyStorageService
php artisan make:migration update_private_key_storage_with_salts
1.2 Correction Challenge Passwords
// Migration correction SCEP passwords
php artisan make:migration secure_scep_challenge_passwords
🔧 Phase 2 - Architecture Nitrokey (3 semaines)
2.1 Développement Agent Client
// Application standalone pour postes clients
// Technologies : PHP CLI + GUI (php-gtk ou Electron wrapper)
2.2 Interface Web Génération Shamir
// Contrôleurs et vues pour génération distribuée
php artisan make:controller ShamirGenerationController
php artisan make:component NitrokeyDeploymentWizard
2.3 Orchestrateur Signature Distribuée
// Service central pour coordination
php artisan make:service DistributedSigningOrchestrator
php artisan make:job ProcessSigningSession
📝 Phase 3 - Tests et Documentation (1 semaine)
3.1 Tests Sécurité
- Tests unitaires nouveau stockage
- Tests d'intégration Nitrokey
- Audit de pénétration
3.2 Documentation Opérationnelle
- Procédures déploiement Nitrokeys
- Guide utilisation agent client
- Procédures de récupération
4. Recommandations Sécurisées
🔐 Stockage des Secrets
// Nouveau modèle de stockage sécurisé
abstract class SecureStorageModel extends Model
{
protected $secureFields = []; // Champs à chiffrer avec salt unique
protected static function boot()
{
parent::boot();
static::saving(function ($model) {
foreach ($model->secureFields as $field) {
if ($model->isDirty($field)) {
$model->encryptSecureField($field);
}
}
});
}
private function encryptSecureField(string $field): void
{
$value = $this->attributes[$field];
if (empty($value)) return;
$salt = random_bytes(32);
$key = $this->deriveFieldKey($field, $salt);
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$encrypted = sodium_crypto_secretbox($value, $nonce, $key);
$this->attributes[$field] = base64_encode($salt . $nonce . $encrypted);
$this->attributes[$field . '_salt'] = base64_encode($salt);
}
}
🔑 Gestion des Mots de Passe
// Service de gestion des mots de passe sécurisé
class SecurePasswordManager
{
// Utiliser Argon2id avec paramètres appropriés
public function hashPassword(string $password, ?string $salt = null): array
{
$salt = $salt ?? random_bytes(32);
$hash = password_hash(
$password . base64_encode($salt), // Salt comme pepper
PASSWORD_ARGON2ID,
[
'memory_cost' => 65536, // 64 MB
'time_cost' => 4, // 4 iterations
'threads' => 3 // 3 threads
]
);
return [
'hash' => $hash,
'salt' => base64_encode($salt),
'algorithm' => 'argon2id'
];
}
public function verifyPassword(string $password, string $hash, string $salt): bool
{
return password_verify($password . $salt, $hash);
}
}
Conclusion
Le système actuel présente des vulnérabilités critiques qui nécessitent une refonte complète de la gestion des clés privées et de l'architecture Nitrokey.
Priorités immédiates : 1. ✅ Implémenter stockage sécurisé avec dérivation de clés uniques 2. ✅ Corriger les mots de passe stockés en clair 3. ✅ Développer architecture distribuée pour Nitrokeys distantes 4. ✅ Intégrer OpenSC/PKCS#11 via agents clients
Estimation effort total : 5 semaines développement + 2 semaines tests/déploiement