diff --git a/app/Http/Controllers/Api/RootCaApiController.php b/app/Http/Controllers/Api/RootCaApiController.php index ec0999e..f005177 100644 --- a/app/Http/Controllers/Api/RootCaApiController.php +++ b/app/Http/Controllers/Api/RootCaApiController.php @@ -78,6 +78,25 @@ class RootCaApiController extends Controller } } + public function renewAll(Request $request) + { + $this->authorizeAdminOrOwner(); + $days = (int) $request->input('days', 3650); + + try { + $this->sslService->bulkRenewStrategy($days); + return response()->json([ + 'status' => 'success', + 'message' => 'Entire CA Chain (Root & Intermediates) renewed successfully.' + ]); + } catch (\Exception $e) { + return response()->json([ + 'status' => 'error', + 'message' => 'Bulk renewal failed: ' . $e->getMessage() + ], 500); + } + } + public function syncCrtOnly(Request $request) { $this->authorizeAdminOrOwner(); diff --git a/app/Services/OpenSslService.php b/app/Services/OpenSslService.php index 7126ca7..0026170 100644 --- a/app/Services/OpenSslService.php +++ b/app/Services/OpenSslService.php @@ -377,8 +377,16 @@ class OpenSslService $issuerCert = null; $issuerKey = $privKey; } else { - // Intermediate is signed by Root - $root = CaCertificate::where('ca_type', 'root')->first(); + // Intermediate is signed by the LATEST Root + $root = CaCertificate::where('ca_type', 'root') + ->where('is_latest', true) + ->first(); + + // Fallback if no is_latest yet (during initial setuptransition) + if (!$root) { + $root = CaCertificate::where('ca_type', 'root')->latest()->first(); + } + if (!$root) throw new \Exception('Root CA not found for signing intermediate renewal.'); $issuerCert = $root->cert_content; $issuerKey = openssl_pkey_get_private($root->key_content); @@ -414,6 +422,67 @@ class OpenSslService if ($configFile && file_exists($configFile)) unlink($configFile); } } + + /** + * Perform a coordinated renewal of the entire CA chain. + * Order: Root -> Intermediates. + */ + public function bulkRenewStrategy(int $days = 3650) + { + // 1. Get current latest Root + $root = CaCertificate::where('ca_type', 'root')->where('is_latest', true)->first(); + if (!$root) throw new \Exception("Current Root CA not found."); + + // 2. Renew Root + $newRoot = $this->executeRenewalFlow($root, $days); + + // 3. Renew Intermediates using the NEW Root + $intermediates = CaCertificate::whereIn('ca_type', ['intermediate_2048', 'intermediate_4096']) + ->where('is_latest', true) + ->get(); + + foreach ($intermediates as $int) { + $this->executeRenewalFlow($int, $days); + } + + // 4. Final Mass Sync + $this->syncAllBundles(); + + return true; + } + + /** + * Internal helper to handle the DB + CDN flow for a single renewal. + */ + private function executeRenewalFlow(CaCertificate $cert, int $days) + { + $newData = $this->renewCaCertificate($cert, $days); + + // Unset latest for others of same type/CN + CaCertificate::where('ca_type', $cert->ca_type) + ->where('common_name', $cert->common_name) + ->update(['is_latest' => false]); + + // Create new + $newCert = CaCertificate::create([ + 'ca_type' => $cert->ca_type, + 'common_name' => $cert->common_name, + 'organization' => $cert->organization, + 'key_content' => $cert->key_content, + 'cert_content' => $newData['cert_content'], + 'serial_number' => $newData['serial_number'], + 'valid_from' => $newData['valid_from'], + 'valid_to' => $newData['valid_to'], + 'is_latest' => true, + ]); + + // Sync to CDN + $this->uploadPublicCertsOnly($newCert, 'both'); + $this->uploadIndividualInstallersOnly($newCert, 'both'); + + return $newCert; + } + /** * Generate Windows Installer (.bat) */ diff --git a/routes/api.php b/routes/api.php index 642524b..254201e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -65,6 +65,7 @@ Route::middleware(['auth:sanctum'])->group(function () { Route::post('/admin/ca-certificates/sync-installers', [RootCaApiController::class, 'syncInstallersOnly']); Route::post('/admin/ca-certificates/sync-bundles', [RootCaApiController::class, 'syncBundlesOnly']); Route::post('/admin/ca-certificates/{certificate}/renew', [RootCaApiController::class, 'renew']); + Route::post('/admin/ca-certificates/renew-all', [RootCaApiController::class, 'renewAll']); Route::post('/admin/ca-certificates/{certificate}/promote', [RootCaApiController::class, 'promote']); // API Keys Management