Storage Backend Abstraction for CA Private Keys

Version: v0.2-alpha Status: Phase 2 Complete (Database + Vaultwarden backends) Next: Phase 3 (Migration tools + Configuration UI)

Overview

The PKI system implements a pluggable storage architecture for CA private keys, enabling enterprise organizations to choose their preferred storage backend based on security requirements, compliance needs, and existing infrastructure.

Supported Backends

Backend Status Description Use Case
Database βœ… Production Direct table storage with libsodium encryption Baseline, self-contained deployments
Vaultwarden βœ… Phase 2 Self-hosted Password Manager API On-premise deployments
Bitwarden Secrets Manager πŸ”œ Future Official PHP SDK integration Enterprise secret management
Shamir Secret Sharing πŸ”œ Phase 3 n-of-m threshold cryptography Distributed trust, high security
Hardware Security Module πŸ”œ Phase 4 YubiKey/NitroKey PKCS#11 Maximum security, compliance

Architecture

Interface Contract

All storage backends implement the SecureKeyStorageInterface:

interface SecureKeyStorageInterface
{
    // Store encrypted CA private key
    public function storePrivateKey(string $caId, string $encryptedKey, array $metadata = []): string;

    // Retrieve encrypted CA private key
    public function retrievePrivateKey(string $caId): string;

    // Delete CA private key from storage
    public function deletePrivateKey(string $caId): bool;

    // Check if CA private key exists
    public function keyExists(string $caId): bool;

    // Migrate key to another backend
    public function migrateKey(string $caId, SecureKeyStorageInterface $newBackend): bool;

    // Get backend name identifier
    public function getBackendName(): string;

    // Health check for backend availability
    public function healthCheck(): array;
}

Database Schema

Three new columns in certificate_authorities table:

-- Storage backend identifier
key_storage_backend VARCHAR(50) DEFAULT 'database'
  COMMENT 'database|bitwarden|vaultwarden|shamir_sss|hsm'

-- External storage reference
key_storage_reference VARCHAR(255) NULL
  COMMENT 'Bitwarden secret ID, HSM slot ID, etc.'

-- Backend-specific metadata
key_storage_metadata JSON NULL
  COMMENT 'project_id, hsm_config, etc.'

Phase 1: Database Backend

Implementation

The DatabaseKeyStorage class wraps the existing SecureKeyStorageService to provide baseline storage functionality.

Location: app/Services/Storage/DatabaseKeyStorage.php

Key Features: - Direct storage in certificate_authorities table - Libsodium secretbox encryption (AEAD) - Password-protected key support - Health monitoring (latency, key count) - Full interface compliance

Security: - Keys encrypted with APP_KEY + unique salt per CA - Password-protected keys use APP_KEY + password hybrid derivation - Argon2id key derivation for resistance against brute force - Automatic corruption detection and flagging

Usage Example

use App\Services\Storage\DatabaseKeyStorage;
use App\Services\SecureKeyStorageService;

$secureKeyStorage = app(SecureKeyStorageService::class);
$storage = new DatabaseKeyStorage($secureKeyStorage);

// Store key
$reference = $storage->storePrivateKey($caId, $encryptedKey, [
    'salt' => $salt,
    'algorithm' => 'sodium_secretbox',
    'password_protected' => false,
]);

// Retrieve key
$encryptedKey = $storage->retrievePrivateKey($caId);

// Health check
$health = $storage->healthCheck();
// ['available' => true, 'latency_ms' => 2, 'keys_stored' => 15]

Phase 2: Vaultwarden/Bitwarden Integration

Implementation Decision

Chosen Approach: Vaultwarden Password Manager API (not Secrets Manager)

Rationale: - Vaultwarden only implements Password Manager API, not Secrets Manager - bitwarden/sdk PHP package only supports Secrets Manager (incompatible with Vaultwarden) - Direct REST API integration provides maximum compatibility - Supports both personal vaults and organization shared vaults

Vaultwarden Architecture

Components: - VaultwardenClient: HTTP REST API wrapper - VaultwardenKeyStorage: SecureKeyStorageInterface implementation

Data Flow:

PKI App β†’ VaultwardenClient β†’ Vaultwarden REST API
              ↓
       [Cipher Creation]
              ↓
       [Attachment Upload: encrypted_key.enc]
              ↓
       [Returns cipher UUID]

Storage Mechanism: 1. Create Secure Note cipher (type 2) 2. Upload encrypted CA key as attachment 3. Store cipher UUID in certificate_authorities.key_storage_reference

VaultwardenClient Features

Location: app/Services/Vaultwarden/VaultwardenClient.php

Capabilities: - Authentication: PBKDF2-SHA256 master password hashing + OAuth2 token - Cipher Management: Create, retrieve, delete secure notes - Attachments: Upload/download encrypted files - Organizations: Support for shared vaults (organizationId + collectionIds) - Health Check: API connectivity and authentication status

Authentication Flow:

// 1. Prelogin to get KDF iterations
POST /accounts/prelogin
{ "email": "user@example.com" }
β†’ { "KdfIterations": 600000 }

// 2. Derive master password hash (PBKDF2)
$masterPasswordHash = hash_pbkdf2('sha256', $password, $email, $iterations, 32);

// 3. Request access token
POST /identity/connect/token
grant_type=password&username=...&password=$masterPasswordHash
β†’ { "access_token": "...", "expires_in": 3600 }

VaultwardenKeyStorage Implementation

Location: app/Services/Storage/VaultwardenKeyStorage.php

Key Methods:

class VaultwardenKeyStorage implements SecureKeyStorageInterface
{
    // Store: Create cipher β†’ Upload attachment β†’ Return cipher UUID
    public function storePrivateKey(string $caId, string $encryptedKey, array $metadata = []): string;

    // Retrieve: Get cipher β†’ Find attachment β†’ Download content
    public function retrievePrivateKey(string $caId): string;

    // Delete: Delete cipher (removes attachments automatically) + Update CA record
    public function deletePrivateKey(string $caId): bool;

    // Check: Verify cipher exists with attachments
    public function keyExists(string $caId): bool;

    // Migrate: Retrieve β†’ Decrypt β†’ Re-encrypt β†’ Store in new backend β†’ Delete old
    public function migrateKey(string $caId, SecureKeyStorageInterface $newBackend): bool;

    // Identity: Returns 'vaultwarden'
    public function getBackendName(): string;

    // Monitoring: Delegate to VaultwardenClient::healthCheck()
    public function healthCheck(): array;
}

Configuration

Environment Variables (.env):

# Vaultwarden API endpoint
VAULTWARDEN_API_URL=http://your-vaultwarden-server.com

# Master account credentials
VAULTWARDEN_EMAIL=vault-account@example.com
VAULTWARDEN_PASSWORD=your-master-password

# Optional: Organization shared vaults
VAULTWARDEN_ORGANIZATION_ID=organization-uuid
VAULTWARDEN_COLLECTION_IDS=collection-uuid-1,collection-uuid-2

# Health check settings
VAULTWARDEN_HEALTH_CHECK_ENABLED=true
VAULTWARDEN_HEALTH_CHECK_INTERVAL=300

Usage Example

use App\Services\Vaultwarden\VaultwardenClient;
use App\Services\Storage\VaultwardenKeyStorage;
use App\Services\SecureKeyStorageService;

// Initialize client
$client = new VaultwardenClient(
    apiUrl: config('vaultwarden.api_url'),
    email: config('vaultwarden.email'),
    password: config('vaultwarden.password'),
    organizationId: config('vaultwarden.organization_id'),
    collectionIds: config('vaultwarden.collection_ids')
);

// Create storage backend
$storage = new VaultwardenKeyStorage(
    app(SecureKeyStorageService::class),
    $client
);

// Store CA private key
$cipherUuid = $storage->storePrivateKey($ca->id, $encryptedKey, [
    'salt' => $salt,
    'algorithm' => 'sodium_secretbox',
    'password_protected' => false,
    'ca_name' => 'Root CA - Production',
]);

// Retrieve key
$encryptedKey = $storage->retrievePrivateKey($ca->id);

// Health check
$health = $storage->healthCheck();
// ['available' => true, 'latency_ms' => 150, 'authenticated' => true]

Cipher Metadata Structure

Each Vaultwarden cipher includes metadata in Notes field:

{
  "ca_id": "1",
  "ca_common_name": "Root CA - Example Corp",
  "encryption_algorithm": "sodium_secretbox",
  "password_protected": false,
  "stored_at": "2025-10-14T12:34:56Z",
  "pkiaas_version": "0.2-alpha"
}

Security Features

Multi-Layer Encryption: 1. PKI Layer: Libsodium secretbox (pre-Vaultwarden) 2. Transport Layer: HTTPS for API calls 3. Vaultwarden Layer: Master password encryption (vault encryption)

Organization Support: - Personal Vault: Single-user access - Shared Vault: Multi-user access via collections - Access Control: Vaultwarden handles user permissions

Migration

Database β†’ Vaultwarden:

$databaseStorage = new DatabaseKeyStorage($secureKeyStorage);
$vaultwardenStorage = new VaultwardenKeyStorage($secureKeyStorage, $client);

// Automatic migration (decrypts + re-encrypts + transfers)
$databaseStorage->migrateKey($caId, $vaultwardenStorage);

// Update CA record
$ca->update([
    'key_storage_backend' => 'vaultwarden',
    'key_storage_reference' => $cipherUuid,  // Set by migrateKey()
]);

Migration Command (planned):

# Interactive wizard
php artisan pki:migrate-storage

# Batch migration to Vaultwarden
php artisan pki:migrate-storage --from=database --to=vaultwarden --all

# Specific CA
php artisan pki:migrate-storage --ca=1 --to=vaultwarden

Testing

Location: tests/Unit/VaultwardenKeyStorageTest.php

Coverage: 21 test cases - Interface implementation - Backend name - Store with cipher creation + attachment upload - Retrieve with cipher fetch + attachment download - Delete with cipher removal + CA cleanup - Key existence checks (with API failure handling) - Health monitoring (success + failure scenarios) - Error handling (missing references, failed uploads, cleanup on error) - Migration prerequisites (password-protected, missing keys)

Mocking Strategy: - VaultwardenClient fully mocked (no real API calls) - Mockery for method expectations - Tests run without external dependencies

Phase 3: Shamir Secret Sharing (Planned)

Concept

Split CA private key into N shares, requiring M shares to reconstruct: - Example: 5-of-7 scheme (7 shares, any 5 can reconstruct) - Use case: Distributed trust, no single point of compromise

Storage Options

  1. Distributed Database: Each share in different DB instance
  2. Hardware Tokens: YubiKey, NitroKey for physical distribution
  3. Geographic Distribution: Shares across multiple data centers

Implementation

Library: php-shamir-secret-sharing or custom implementation

Metadata Example:

{
  "scheme": "5-of-7",
  "shares": [
    {"id": 1, "location": "db-us-east"},
    {"id": 2, "location": "db-eu-west"},
    ...
  ]
}

Phase 4: Hardware Security Module (Planned)

Supported HSMs

  • YubiKey (USB-A, USB-C, NFC)
  • NitroKey (Open-source alternative)
  • PKCS#11 Standard: Generic HSM support

Benefits

  • Private key never leaves hardware device
  • Tamper-resistant
  • FIPS 140-2 compliance
  • Suitable for root CAs

Implementation

Library: pkcs11-php extension

Metadata Example:

{
  "hsm_type": "yubikey",
  "slot_id": 1,
  "pin_protected": true,
  "serial_number": "12345678"
}

Migration Between Backends

Automated Migration

The migrateKey() interface method enables seamless backend migration:

// Migrate from Database to Bitwarden
$databaseStorage = new DatabaseKeyStorage($secureKeyStorage);
$bitwardenStorage = new BitwardenKeyStorage($bitwardenClient);

$databaseStorage->migrateKey($caId, $bitwardenStorage);

Migration Process: 1. Retrieve encrypted key from source backend 2. Decrypt with password (if required) 3. Re-encrypt for target backend 4. Store in target backend 5. Delete from source backend 6. Update key_storage_backend in database

Manual Migration Command

# Interactive wizard
php artisan pki:migrate-storage

# Batch migration
php artisan pki:migrate-storage --from=database --to=bitwarden --all

# Specific CA
php artisan pki:migrate-storage --ca=1 --to=shamir_sss

Security Considerations

Encryption Layers

  1. Application Layer: Libsodium secretbox encryption
  2. Transport Layer: TLS for API calls
  3. Storage Layer: Backend-specific encryption (Bitwarden, HSM)

Password-Protected Keys

Password-protected CAs require password during migration:

php artisan pki:migrate-storage --ca=1 --to=bitwarden --password
# Prompt: Enter CA private key password: ****

Audit Logging

All storage operations logged to pki-compliance channel: - Key storage/retrieval/deletion - Backend migration - Health check failures - Access attempts

Health Monitoring

Each backend implements health checks:

$health = $storage->healthCheck();

[
    'available' => true,          // Backend reachable
    'latency_ms' => 15,           // Response time
    'error' => null,              // Error message (if unavailable)
    'backend' => 'database',      // Backend identifier
    'keys_stored' => 42,          // Total keys (database only)
]

Monitoring Integration: - Prometheus metrics export - Dashboard widgets - Alert triggers (latency > 1000ms)

Testing

Unit Tests

DatabaseKeyStorage

Location: tests/Unit/DatabaseKeyStorageTest.php

Coverage: 11 test cases - Interface implementation - Backend name - Store/retrieve/delete operations - Key existence checks - Health monitoring - Error handling (missing metadata, nonexistent keys)

VaultwardenKeyStorage

Location: tests/Unit/VaultwardenKeyStorageTest.php

Coverage: 21 test cases - Interface implementation - Backend name - Store with cipher creation + attachment upload - Retrieve with cipher fetch + attachment download - Delete with cipher removal + CA cleanup - Key existence checks (with API failure handling) - Health monitoring (success + failure scenarios) - Error handling (missing references, failed uploads, cleanup on error) - Migration prerequisites (password-protected, missing keys)

Mocking Strategy: VaultwardenClient fully mocked (Mockery)

Integration Tests

Planned for Phase 3: - Cross-backend migration (Database ↔ Vaultwarden) - Password-protected key migration with password prompt - Real Vaultwarden instance integration - Health check reliability under load

Best Practices

Backend Selection Guidelines

Requirement Recommended Backend
Simple deployment Database
Enterprise secret management Bitwarden Secrets Manager
Self-hosted Vaultwarden
Maximum security HSM (YubiKey)
Distributed trust Shamir SSS
Compliance (FIPS 140-2) HSM

Migration Strategy

  1. Test Environment First: Validate migration with test CAs
  2. Backup Before Migration: Export CA certificates and metadata
  3. Gradual Rollout: Migrate non-critical CAs first
  4. Monitor Health: Check backend availability post-migration
  5. Document Recovery: Maintain rollback procedures

Roadmap

v0.2-alpha (Current - Completed)

  • βœ… Phase 1.1: Interface contract (SecureKeyStorageInterface)
  • βœ… Phase 1.2: Database backend (DatabaseKeyStorage)
  • βœ… Phase 1.3: Database migration (3 new columns)
  • βœ… Phase 1.4: Unit tests (11 test cases)
  • βœ… Phase 2.1: Vaultwarden client (REST API wrapper)
  • βœ… Phase 2.2: Vaultwarden backend (VaultwardenKeyStorage)
  • βœ… Phase 2.3: Configuration (.env + .env.example)
  • βœ… Phase 2.4: Unit tests (21 test cases)
  • βœ… Phase 2.5: Documentation update

v0.3-alpha (Q1 2026)

  • πŸ”œ Phase 2.6: Configuration UI (backend selection in CA creation)
  • πŸ”œ Phase 2.7: Migration command (php artisan pki:migrate-storage)
  • πŸ”œ Integration tests (cross-backend migration)
  • πŸ”œ Bitwarden Secrets Manager support (optional)

v0.4-alpha (Q2 2026)

  • πŸ”œ Phase 3: Shamir Secret Sharing (n-of-m)
  • πŸ”œ Phase 4: HSM support (YubiKey/NitroKey)

References

Vous n'avez pas envie de la manager ?

DΓ©couvrir notre offre PKI As A Service