From b23fe4b00ec9f57af678494a656a258a58853560 Mon Sep 17 00:00:00 2001 From: dyzulk <66510723+dyzulk@users.noreply.github.com> Date: Tue, 6 Jan 2026 14:58:58 +0700 Subject: [PATCH] feat: implement OS-specific installer sync and CDN redirect enhancements --- .../Controllers/Api/PublicCaController.php | 37 ++++- app/Models/CaCertificate.php | 3 + app/Services/OpenSslService.php | 147 +++++++++++++++++- ...23_123913_create_ca_certificates_table.php | 14 +- routes/api.php | 1 + 5 files changed, 199 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Api/PublicCaController.php b/app/Http/Controllers/Api/PublicCaController.php index 041489e..f7e5006 100644 --- a/app/Http/Controllers/Api/PublicCaController.php +++ b/app/Http/Controllers/Api/PublicCaController.php @@ -19,15 +19,19 @@ class PublicCaController extends Controller $caTypes = ['root', 'intermediate_2048', 'intermediate_4096']; $certificates = CaCertificate::whereIn('ca_type', $caTypes) - ->get(['common_name', 'ca_type', 'serial_number', 'valid_to', 'cert_content', 'cert_path']) + ->get(['common_name', 'ca_type', 'serial_number', 'valid_to', 'cert_content', 'cert_path', 'der_path', 'bat_path', 'mac_path', 'linux_path', 'last_synced_at']) ->map(function ($cert) { return [ 'name' => $cert->common_name, 'type' => $cert->ca_type, 'serial' => $cert->serial_number, 'expires_at' => $cert->valid_to->toIso8601String(), + 'last_synced_at' => $cert->last_synced_at ? $cert->last_synced_at->toIso8601String() : null, 'cdn_url' => $cert->cert_path ? Storage::disk('r2-public')->url($cert->cert_path) : null, 'der_cdn_url' => $cert->der_path ? Storage::disk('r2-public')->url($cert->der_path) : null, + 'bat_cdn_url' => $cert->bat_path ? Storage::disk('r2-public')->url($cert->bat_path) : null, + 'mac_cdn_url' => $cert->mac_path ? Storage::disk('r2-public')->url($cert->mac_path) : null, + 'linux_cdn_url' => $cert->linux_path ? Storage::disk('r2-public')->url($cert->linux_path) : null, ]; }); @@ -88,6 +92,11 @@ class PublicCaController extends Controller $cert = CaCertificate::where('serial_number', $serial)->firstOrFail(); $cert->increment('download_count'); $cert->update(['last_downloaded_at' => now()]); + + if ($cert->bat_path) { + return redirect()->away(Storage::disk('r2-public')->url($cert->bat_path)); + } + $store = $cert->ca_type === 'root' ? 'Root' : 'CA'; $filename = preg_replace('/[^a-zA-Z0-9_-]/', '_', $cert->common_name); @@ -120,6 +129,10 @@ class PublicCaController extends Controller $cert = CaCertificate::where('serial_number', $serial)->firstOrFail(); $cert->increment('download_count'); $cert->update(['last_downloaded_at' => now()]); + + if ($cert->mac_path) { + return redirect()->away(Storage::disk('r2-public')->url($cert->mac_path)); + } // Extract Base64 payload $pem = $cert->cert_content; @@ -181,4 +194,26 @@ class PublicCaController extends Controller ->header('Content-Type', 'application/x-apple-aspen-config') ->header('Content-Disposition', 'attachment; filename="' . $name . '.mobileconfig"'); } + + /** + * Download Linux Installer (.sh) + */ + public function downloadLinux($serial) + { + $cert = CaCertificate::where('serial_number', $serial)->firstOrFail(); + $cert->increment('download_count'); + $cert->update(['last_downloaded_at' => now()]); + + if ($cert->linux_path) { + return redirect()->away(Storage::disk('r2-public')->url($cert->linux_path)); + } + + // Fallback or dynamic generation if needed (already in Service) + $sslService = app(\App\Services\OpenSslService::class); + $script = $sslService->generateLinuxInstaller($cert); + + return response($script) + ->header('Content-Type', 'application/x-sh') + ->header('Content-Disposition', 'attachment; filename="install-' . Str::slug($cert->common_name) . '.sh"'); + } } diff --git a/app/Models/CaCertificate.php b/app/Models/CaCertificate.php index 9ce9a34..28a9073 100644 --- a/app/Models/CaCertificate.php +++ b/app/Models/CaCertificate.php @@ -25,6 +25,9 @@ class CaCertificate extends Model 'valid_to', 'cert_path', 'der_path', + 'bat_path', + 'mac_path', + 'linux_path', 'last_synced_at', 'download_count', 'last_downloaded_at' diff --git a/app/Services/OpenSslService.php b/app/Services/OpenSslService.php index 869aa77..61b5ddf 100644 --- a/app/Services/OpenSslService.php +++ b/app/Services/OpenSslService.php @@ -412,7 +412,125 @@ class OpenSslService } } /** - * Upload CA certificate (public) to R2 CDN in both PEM and DER formats. + * Generate Windows Installer (.bat) + */ + 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"); + + return "@echo off\n" . + "echo TrustLab - Installing 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" . + " echo Error: Failed to download certificate.\n" . + " pause\n" . + " exit /b 1\n" . + ")\n" . + "certutil -addstore -f \"Root\" \"%TEMP_CERT%\"\n" . + "del \"%TEMP_CERT%\"\n" . + "echo Installation Complete.\n" . + "pause"; + } + + /** + * Generate macOS Configuration Profile (.mobileconfig) + */ + public function generateMacInstaller(CaCertificate $cert): string + { + $certBase64 = base64_encode($cert->cert_content); + $payloadId = "com.trustlab.ca." . Str::slug($cert->common_name); + $uuid1 = Str::uuid()->toString(); + $uuid2 = Str::uuid()->toString(); + + return "\n" . + "\n" . + "\n" . + "\n" . + " PayloadContent\n" . + " \n" . + " \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" . + " {$payloadId}.cert\n" . + " PayloadType\n" . + " com.apple.security.root\n" . + " PayloadUUID\n" . + " {$uuid2}\n" . + " PayloadVersion\n" . + " 1\n" . + " \n" . + " \n" . + " PayloadDescription\n" . + " TrustLab CA Root Installation\n" . + " PayloadDisplayName\n" . + " TrustLab CA: {$cert->common_name}\n" . + " PayloadIdentifier\n" . + " {$payloadId}\n" . + " PayloadRemovalDisallowed\n" . + " \n" . + " PayloadType\n" . + " Configuration\n" . + " PayloadUUID\n" . + " {$uuid1}\n" . + " PayloadVersion\n" . + " 1\n" . + "\n" . + ""; + } + + /** + * Generate Linux Installer (.sh) + */ + public function generateLinuxInstaller(CaCertificate $cert): string + { + $cdnUrl = $cert->cert_path ? Storage::disk('r2-public')->url($cert->cert_path) : url("/api/public/ca/{$cert->uuid}/download/pem"); + $filename = Str::slug($cert->common_name) . ".crt"; + + return "#!/bin/bash\n" . + "echo \"TrustLab - Installing CA Certificate: {$cert->common_name}\"\n" . + "if [ \"\$EUID\" -ne 0 ]; then echo \"Please run as root (sudo)\"; exit 1; fi\n" . + "TEMP_CERT=\"/tmp/trustlab-{$cert->uuid}.crt\"\n" . + "curl -sL \"{$cdnUrl}\" -o \"\$TEMP_CERT\"\n" . + "if [ ! -f \"\$TEMP_CERT\" ]; then echo \"Failed to download cert\"; exit 1; fi\n\n" . + "# Ubuntu/Debian\n" . + "if [ -d /usr/local/share/ca-certificates ]; then\n" . + " cp \"\$TEMP_CERT\" \"/usr/local/share/ca-certificates/{$filename}\"\n" . + " update-ca-certificates\n" . + "# RHEL/CentOS/Fedora\n" . + "elif [ -d /etc/pki/ca-trust/source/anchors ]; then\n" . + " cp \"\$TEMP_CERT\" \"/etc/pki/ca-trust/source/anchors/{$filename}\"\n" . + " update-ca-trust extract\n" . + "# Arch Linux\n" . + "elif [ -d /etc/ca-certificates/trust-source/anchors ]; then\n" . + " cp \"\$TEMP_CERT\" \"/etc/ca-certificates/trust-source/anchors/{$filename}\"\n" . + " trust extract-compat\n" . + "else\n" . + " echo \"Unsupported Linux distribution for automatic install.\"\n" . + " echo \"Please manually install \$TEMP_CERT\"\n" . + " exit 1\n" . + "fi\n" . + "rm \"\$TEMP_CERT\"\n" . + "echo \"Installation Complete.\"\n"; + } + + private function getPublicKeyCdnUrl(CaCertificate $cert, $ext) + { + $path = $ext === 'der' ? $cert->der_path : $cert->cert_path; + return $path ? Storage::disk('r2-public')->url($path) : null; + } + + /** + * Upload CA certificate (public) to R2 CDN in 5 formats. */ public function uploadToCdn(CaCertificate $cert) { @@ -420,6 +538,9 @@ class OpenSslService $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, [ @@ -442,9 +563,33 @@ class OpenSslService '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() ]); diff --git a/database/migrations/2025_12_23_123913_create_ca_certificates_table.php b/database/migrations/2025_12_23_123913_create_ca_certificates_table.php index 8f516a0..ae1564c 100644 --- a/database/migrations/2025_12_23_123913_create_ca_certificates_table.php +++ b/database/migrations/2025_12_23_123913_create_ca_certificates_table.php @@ -23,6 +23,9 @@ return new class extends Migration // CDN Integration Columns $table->string('cert_path')->nullable(); $table->string('der_path')->nullable(); + $table->string('bat_path')->nullable(); + $table->string('mac_path')->nullable(); + $table->string('linux_path')->nullable(); $table->timestamp('last_synced_at')->nullable(); $table->string('common_name')->nullable(); @@ -43,8 +46,17 @@ return new class extends Migration if (!Schema::connection('mysql_ca')->hasColumn('ca_certificates', 'der_path')) { $table->string('der_path')->nullable()->after('cert_path'); } + if (!Schema::connection('mysql_ca')->hasColumn('ca_certificates', 'bat_path')) { + $table->string('bat_path')->nullable()->after('der_path'); + } + if (!Schema::connection('mysql_ca')->hasColumn('ca_certificates', 'mac_path')) { + $table->string('mac_path')->nullable()->after('bat_path'); + } + if (!Schema::connection('mysql_ca')->hasColumn('ca_certificates', 'linux_path')) { + $table->string('linux_path')->nullable()->after('mac_path'); + } if (!Schema::connection('mysql_ca')->hasColumn('ca_certificates', 'last_synced_at')) { - $table->timestamp('last_synced_at')->nullable()->after('der_path'); + $table->timestamp('last_synced_at')->nullable()->after('linux_path'); } }); } diff --git a/routes/api.php b/routes/api.php index 6e743b6..babb7f6 100644 --- a/routes/api.php +++ b/routes/api.php @@ -24,6 +24,7 @@ Route::get('/public/ca-certificates', [PublicCaController::class, 'index']); Route::get('/public/ca-certificates/{serial}/download', [PublicCaController::class, 'download']); Route::get('/public/ca-certificates/{serial}/download/windows', [PublicCaController::class, 'downloadWindows']); Route::get('/public/ca-certificates/{serial}/download/mac', [PublicCaController::class, 'downloadMac']); +Route::get('/public/ca-certificates/{serial}/download/linux', [PublicCaController::class, 'downloadLinux']); Route::post('/public/inquiries', [\App\Http\Controllers\Api\InquiryController::class, 'store']); Route::get('/public/legal-pages', [\App\Http\Controllers\Api\LegalPageController::class, 'index']); Route::get('/public/legal-pages/{slug}', [\App\Http\Controllers\Api\LegalPageController::class, 'show']);