Audit Sécurité PKIaaS - Stockage des Clés et Mots de Passe

Résumé Exécutif

🔴 Vulnérabilités Critiques Identifiées

  1. Stockage des clés privées : Usage de Crypt::encryptString() Laravel (clé unique système)
  2. Mots de passe d'approbation : Hashage correct mais pas de salt explicite personnalisé
  3. Challenge passwords SCEP : Stockage en clair dans la base de données
  4. 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