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