diff --git a/app/Http/Controllers/Api/PublicCaController.php b/app/Http/Controllers/Api/PublicCaController.php index f7e5006..5eb3f9f 100644 --- a/app/Http/Controllers/Api/PublicCaController.php +++ b/app/Http/Controllers/Api/PublicCaController.php @@ -37,7 +37,12 @@ class PublicCaController extends Controller return response()->json([ 'success' => true, - 'data' => $certificates + 'data' => $certificates, + 'bundle_urls' => [ + 'linux' => Storage::disk('r2-public')->url('ca/bundles/trustlab-all.sh'), + 'windows' => Storage::disk('r2-public')->url('ca/bundles/trustlab-all.bat'), + 'macos' => Storage::disk('r2-public')->url('ca/bundles/trustlab-all.mobileconfig'), + ] ]); } diff --git a/app/Http/Controllers/Api/RootCaApiController.php b/app/Http/Controllers/Api/RootCaApiController.php index 72834f8..f7ce5e4 100644 --- a/app/Http/Controllers/Api/RootCaApiController.php +++ b/app/Http/Controllers/Api/RootCaApiController.php @@ -60,6 +60,53 @@ class RootCaApiController extends Controller } } + public function syncCrtOnly() + { + $this->authorizeAdminOrOwner(); + try { + $certificates = CaCertificate::all(); + $count = 0; + foreach ($certificates as $cert) { + if ($this->sslService->uploadPublicCertsOnly($cert)) { + $count++; + } + } + return response()->json(['status' => 'success', 'message' => "Successfully synced {$count} CRT files."]); + } catch (\Exception $e) { + return response()->json(['status' => 'error', 'message' => 'Sync failed: ' . $e->getMessage()], 500); + } + } + + public function syncInstallersOnly() + { + $this->authorizeAdminOrOwner(); + try { + $certificates = CaCertificate::all(); + $count = 0; + foreach ($certificates as $cert) { + if ($this->sslService->uploadIndividualInstallersOnly($cert)) { + $count++; + } + } + return response()->json(['status' => 'success', 'message' => "Successfully synced {$count} installer sets."]); + } catch (\Exception $e) { + return response()->json(['status' => 'error', 'message' => 'Sync failed: ' . $e->getMessage()], 500); + } + } + + public function syncBundlesOnly() + { + $this->authorizeAdminOrOwner(); + try { + if ($this->sslService->syncAllBundles()) { + return response()->json(['status' => 'success', 'message' => "Successfully synced All-in-One bundles."]); + } + return response()->json(['status' => 'error', 'message' => 'No certificates found to bundle.'], 404); + } catch (\Exception $e) { + return response()->json(['status' => 'error', 'message' => 'Sync failed: ' . $e->getMessage()], 500); + } + } + public function syncToCdn() { $this->authorizeAdminOrOwner(); @@ -76,7 +123,7 @@ class RootCaApiController extends Controller return response()->json([ 'status' => 'success', - 'message' => "Successfully synced {$count} certificates to CDN." + 'message' => "Successfully synced everything ({$count} certs + bundles) to CDN." ]); } catch (\Exception $e) { return response()->json([ diff --git a/app/Services/OpenSslService.php b/app/Services/OpenSslService.php index 61b5ddf..b5fe787 100644 --- a/app/Services/OpenSslService.php +++ b/app/Services/OpenSslService.php @@ -416,12 +416,12 @@ class OpenSslService */ public function generateWindowsInstaller(CaCertificate $cert): string { - $cdnUrl = $this->getPublicKeyCdnUrl($cert, 'cer'); // Windows prefer .cer (DER or PEM) - // Note: certutil can import PEM/CRT as well. We'll use the .crt (PEM) from CDN. $cdnUrl = $cert->cert_path ? Storage::disk('r2-public')->url($cert->cert_path) : url("/api/public/ca/{$cert->uuid}/download/pem"); + $typeLabel = $cert->ca_type === 'root' ? 'Root' : 'Intermediate'; + $store = $cert->ca_type === 'root' ? 'Root' : 'CA'; return "@echo off\n" . - "echo TrustLab - Installing Certificate: {$cert->common_name}\n" . + "echo TrustLab - Installing {$typeLabel} CA Certificate: {$cert->common_name}\n" . "set \"TEMP_CERT=%TEMP%\\trustlab-ca-{$cert->uuid}.crt\"\n" . "curl -sL \"{$cdnUrl}\" -o \"%TEMP_CERT%\"\n" . "if %ERRORLEVEL% NEQ 0 (\n" . @@ -429,7 +429,7 @@ class OpenSslService " pause\n" . " exit /b 1\n" . ")\n" . - "certutil -addstore -f \"Root\" \"%TEMP_CERT%\"\n" . + "certutil -addstore -f \"{$store}\" \"%TEMP_CERT%\"\n" . "del \"%TEMP_CERT%\"\n" . "echo Installation Complete.\n" . "pause"; @@ -444,6 +444,9 @@ class OpenSslService $payloadId = "com.trustlab.ca." . Str::slug($cert->common_name); $uuid1 = Str::uuid()->toString(); $uuid2 = Str::uuid()->toString(); + + // Root CAs use 'com.apple.security.root', Intermediate CAs use 'com.apple.security.pkcs1' (intermediate) + $payloadType = $cert->ca_type === 'root' ? 'com.apple.security.root' : 'com.apple.security.pkcs1'; return "\n" . "\n" . @@ -463,7 +466,7 @@ class OpenSslService " PayloadIdentifier\n" . " {$payloadId}.cert\n" . " PayloadType\n" . - " com.apple.security.root\n" . + " {$payloadType}\n" . " PayloadUUID\n" . " {$uuid2}\n" . " PayloadVersion\n" . @@ -471,7 +474,7 @@ class OpenSslService " \n" . " \n" . " PayloadDescription\n" . - " TrustLab CA Root Installation\n" . + " TrustLab CA Installation\n" . " PayloadDisplayName\n" . " TrustLab CA: {$cert->common_name}\n" . " PayloadIdentifier\n" . @@ -523,76 +526,222 @@ class OpenSslService "echo \"Installation Complete.\"\n"; } - private function getPublicKeyCdnUrl(CaCertificate $cert, $ext) + /** + * Upload only PEM/DER (The CRT files) to CDN. + */ + public function uploadPublicCertsOnly(CaCertificate $cert) { - $path = $ext === 'der' ? $cert->der_path : $cert->cert_path; - return $path ? Storage::disk('r2-public')->url($path) : null; + $baseFilename = 'ca/' . Str::slug($cert->common_name) . '-' . $cert->uuid; + $pemFilename = $baseFilename . '.crt'; + $derFilename = $baseFilename . '.der'; + + // 1. Upload PEM (.crt) + Storage::disk('r2-public')->put($pemFilename, $cert->cert_content, [ + 'visibility' => 'public', + 'ContentType' => 'application/x-x509-ca-cert' + ]); + + // 2. Convert to DER and Upload (.der) + $lines = explode("\n", trim($cert->cert_content)); + $payload = ''; + foreach ($lines as $line) { + if (!str_starts_with($line, '-----')) { + $payload .= trim($line); + } + } + $derContent = base64_decode($payload); + + Storage::disk('r2-public')->put($derFilename, $derContent, [ + 'visibility' => 'public', + 'ContentType' => 'application/x-x509-ca-cert' + ]); + + $cert->update([ + 'cert_path' => $pemFilename, + 'der_path' => $derFilename, + 'last_synced_at' => now() + ]); + + return true; } /** - * Upload CA certificate (public) to R2 CDN in 5 formats. + * Upload individual installers (SH, BAT, MAC) to CDN. + */ + public function uploadIndividualInstallersOnly(CaCertificate $cert) + { + $baseFilename = 'ca/' . Str::slug($cert->common_name) . '-' . $cert->uuid; + $batFilename = $baseFilename . '.bat'; + $macFilename = $baseFilename . '.mobileconfig'; + $linuxFilename = $baseFilename . '.sh'; + + $cacheControl = 'no-cache, no-store, must-revalidate'; + + // 3. Generate and Upload Windows Installer (.bat) + $batContent = $this->generateWindowsInstaller($cert); + Storage::disk('r2-public')->put($batFilename, $batContent, [ + 'visibility' => 'public', + 'ContentType' => 'application/x-msdos-program', + 'CacheControl' => $cacheControl + ]); + + // 4. Generate and Upload macOS Profile (.mobileconfig) + $macContent = $this->generateMacInstaller($cert); + Storage::disk('r2-public')->put($macFilename, $macContent, [ + 'visibility' => 'public', + 'ContentType' => 'application/x-apple-aspen-config', + 'CacheControl' => $cacheControl + ]); + + // 5. Generate and Upload Linux Script (.sh) + $linuxContent = $this->generateLinuxInstaller($cert); + Storage::disk('r2-public')->put($linuxFilename, $linuxContent, [ + 'visibility' => 'public', + 'ContentType' => 'application/x-sh', + 'CacheControl' => $cacheControl + ]); + + $cert->update([ + 'bat_path' => $batFilename, + 'mac_path' => $macFilename, + 'linux_path' => $linuxFilename, + 'last_synced_at' => now() + ]); + + return true; + } + + /** + * Generate Global Bundles (Installer Sapujagat) + */ + public function syncAllBundles() + { + $certificates = CaCertificate::all(); + if ($certificates->isEmpty()) return false; + + $cacheControl = 'no-cache, no-store, must-revalidate'; + + // 1. Linux Bundle (.sh) + $shContent = "#!/bin/bash\n" . + "echo \"TrustLab - Installing all CA Certificates...\"\n" . + "if [ \"\$EUID\" -ne 0 ]; then echo \"Please run as root (sudo)\"; exit 1; fi\n"; + + foreach ($certificates as $cert) { + $cdnUrl = $cert->cert_path ? Storage::disk('r2-public')->url($cert->cert_path) : null; + if (!$cdnUrl) continue; + + $filename = Str::slug($cert->common_name) . ".crt"; + $shContent .= "echo \"Downloading {$cert->common_name}...\"\n" . + "curl -sL \"{$cdnUrl}\" -o \"/tmp/{$filename}\"\n" . + "if [ -d /usr/local/share/ca-certificates ]; then cp \"/tmp/{$filename}\" \"/usr/local/share/ca-certificates/\"; fi\n" . + "if [ -d /etc/pki/ca-trust/source/anchors ]; then cp \"/tmp/{$filename}\" \"/etc/pki/ca-trust/source/anchors/\"; fi\n" . + "if [ -d /etc/ca-certificates/trust-source/anchors ]; then cp \"/tmp/{$filename}\" \"/etc/ca-certificates/trust-source/anchors/\"; fi\n"; + } + + $shContent .= "echo \"Updating CA store...\"\n" . + "if command -v update-ca-certificates >/dev/null; then update-ca-certificates; fi\n" . + "if command -v update-ca-trust >/dev/null; then update-ca-trust extract; fi\n" . + "if command -v trust >/dev/null; then trust extract-compat; fi\n" . + "echo \"All certificates installed.\"\n"; + + Storage::disk('r2-public')->put('ca/bundles/trustlab-all.sh', $shContent, [ + 'visibility' => 'public', + 'ContentType' => 'application/x-sh', + 'CacheControl' => $cacheControl + ]); + + // 2. Windows Bundle (.bat) + $batContent = "@echo off\n" . + "echo TrustLab - Installing all CA Certificates...\n"; + + foreach ($certificates as $cert) { + $cdnUrl = $cert->cert_path ? Storage::disk('r2-public')->url($cert->cert_path) : null; + if (!$cdnUrl) continue; + + $store = $cert->ca_type === 'root' ? 'Root' : 'CA'; + $batContent .= "echo Installing {$cert->common_name} to {$store} store...\n" . + "curl -sL \"{$cdnUrl}\" -o \"%TEMP%\\tl-{$cert->uuid}.crt\"\n" . + "certutil -addstore -f \"{$store}\" \"%TEMP%\\tl-{$cert->uuid}.crt\"\n" . + "del \"%TEMP%\\tl-{$cert->uuid}.crt\"\n"; + } + $batContent .= "echo Installation Complete.\npause"; + + Storage::disk('r2-public')->put('ca/bundles/trustlab-all.bat', $batContent, [ + 'visibility' => 'public', + 'ContentType' => 'application/x-msdos-program', + 'CacheControl' => $cacheControl + ]); + + // 3. macOS Bundle (.mobileconfig) + $uuid1 = Str::uuid()->toString(); + $payloadContent = ""; + + foreach ($certificates as $cert) { + $certBase64 = base64_encode($cert->cert_content); + $uuidSub = Str::uuid()->toString(); + $payloadType = $cert->ca_type === 'root' ? 'com.apple.security.root' : 'com.apple.security.pkcs1'; + + $payloadContent .= " \n" . + " PayloadCertificateFileName\n" . + " {$cert->common_name}.crt\n" . + " PayloadContent\n" . + " {$certBase64}\n" . + " PayloadDescription\n" . + " TrustLab CA Certificate\n" . + " PayloadDisplayName\n" . + " {$cert->common_name}\n" . + " PayloadIdentifier\n" . + " com.trustlab.bundle.{$cert->uuid}\n" . + " PayloadType\n" . + " {$payloadType}\n" . + " PayloadUUID\n" . + " {$uuidSub}\n" . + " PayloadVersion\n" . + " 1\n" . + " \n"; + } + + $macContent = "\n" . + "\n" . + "\n" . + "\n" . + " PayloadContent\n" . + " \n" . $payloadContent . " \n" . + " PayloadDescription\n" . + " TrustLab All-in-One CA Bundle\n" . + " PayloadDisplayName\n" . + " TrustLab CA Bundle\n" . + " PayloadIdentifier\n" . + " com.trustlab.ca.bundle\n" . + " PayloadRemovalDisallowed\n" . + " \n" . + " PayloadType\n" . + " Configuration\n" . + " PayloadUUID\n" . + " {$uuid1}\n" . + " PayloadVersion\n" . + " 1\n" . + "\n" . + ""; + + Storage::disk('r2-public')->put('ca/bundles/trustlab-all.mobileconfig', $macContent, [ + 'visibility' => 'public', + 'ContentType' => 'application/x-apple-aspen-config', + 'CacheControl' => $cacheControl + ]); + + return true; + } + + /** + * Legacy/Full Upload (Uploads everything) */ public function uploadToCdn(CaCertificate $cert) { try { - $baseFilename = 'ca/' . Str::slug($cert->common_name) . '-' . $cert->uuid; - $pemFilename = $baseFilename . '.crt'; - $derFilename = $baseFilename . '.der'; - $batFilename = $baseFilename . '.bat'; - $macFilename = $baseFilename . '.mobileconfig'; - $linuxFilename = $baseFilename . '.sh'; - - // 1. Upload PEM (.crt) - Storage::disk('r2-public')->put($pemFilename, $cert->cert_content, [ - 'visibility' => 'public', - 'ContentType' => 'application/x-x509-ca-cert' - ]); - - // 2. Convert to DER and Upload (.der) - $lines = explode("\n", trim($cert->cert_content)); - $payload = ''; - foreach ($lines as $line) { - if (!str_starts_with($line, '-----')) { - $payload .= trim($line); - } - } - $derContent = base64_decode($payload); - - Storage::disk('r2-public')->put($derFilename, $derContent, [ - 'visibility' => 'public', - 'ContentType' => 'application/x-x509-ca-cert' - ]); - - // 3. Generate and Upload Windows Installer (.bat) - $batContent = $this->generateWindowsInstaller($cert); - Storage::disk('r2-public')->put($batFilename, $batContent, [ - 'visibility' => 'public', - 'ContentType' => 'application/x-msdos-program' - ]); - - // 4. Generate and Upload macOS Profile (.mobileconfig) - $macContent = $this->generateMacInstaller($cert); - Storage::disk('r2-public')->put($macFilename, $macContent, [ - 'visibility' => 'public', - 'ContentType' => 'application/x-apple-aspen-config' - ]); - - // 5. Generate and Upload Linux Script (.sh) - $linuxContent = $this->generateLinuxInstaller($cert); - Storage::disk('r2-public')->put($linuxFilename, $linuxContent, [ - 'visibility' => 'public', - 'ContentType' => 'application/x-sh' - ]); - - $cert->update([ - 'cert_path' => $pemFilename, - 'der_path' => $derFilename, - 'bat_path' => $batFilename, - 'mac_path' => $macFilename, - 'linux_path' => $linuxFilename, - 'last_synced_at' => now() - ]); - + $this->uploadPublicCertsOnly($cert); + $this->uploadIndividualInstallersOnly($cert); + $this->syncAllBundles(); return true; } catch (\Exception $e) { \Log::error("Failed to upload CA to R2: " . $e->getMessage()); diff --git a/routes/api.php b/routes/api.php index babb7f6..ee2b2ed 100644 --- a/routes/api.php +++ b/routes/api.php @@ -61,6 +61,9 @@ Route::middleware(['auth:sanctum'])->group(function () { // Root CA Management (Admin Only) Route::get('/admin/ca-certificates', [RootCaApiController::class, 'index']); Route::post('/admin/ca-certificates/sync-cdn', [RootCaApiController::class, 'syncToCdn']); + Route::post('/admin/ca-certificates/sync-crt', [RootCaApiController::class, 'syncCrtOnly']); + 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']); // API Keys Management