diff --git a/app/Http/Controllers/Admin/ReleaseController.php b/app/Http/Controllers/Admin/ReleaseController.php index a68ff65..2d2b5e2 100644 --- a/app/Http/Controllers/Admin/ReleaseController.php +++ b/app/Http/Controllers/Admin/ReleaseController.php @@ -68,7 +68,17 @@ final class ReleaseController extends Controller 'version' => ['required','string','max:40'], 'build_number' => ['required','integer','min:1'], 'channel' => ['required', Rule::in(['stable','beta'])], 'minimum_macos' => ['nullable','string','max:30'], 'release_notes' => ['nullable','string','max:20000'], 'sparkle_signature' => ['nullable','string','max:2000'], - 'package' => [$release->exists ? 'nullable' : 'required','file','max:1048576','mimes:dmg,zip'], + 'package' => [ + $release->exists ? 'nullable' : 'required', + 'file', + 'max:1048576', + function (string $attribute, $value, \Closure $fail): void { + $extension = strtolower($value->getClientOriginalExtension()); + if (! in_array($extension, ['dmg', 'zip'], true)) { + $fail('The package must be a DMG or ZIP file.'); + } + }, + ], 'is_published' => ['nullable','boolean'], 'is_featured' => ['nullable','boolean'], ]); $data['is_published'] = $request->boolean('is_published'); diff --git a/app/Services/Licensing/LicenseTokenVerifier.php b/app/Services/Licensing/LicenseTokenVerifier.php index fa497fc..a13ba47 100755 --- a/app/Services/Licensing/LicenseTokenVerifier.php +++ b/app/Services/Licensing/LicenseTokenVerifier.php @@ -14,10 +14,11 @@ final class LicenseTokenVerifier } $payload = $this->decode($parts[1]); $signature = $this->decode($parts[2]); - $public = base64_decode((string) config('sonardesk.public_key'), true); - if ($public === false || strlen($public) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) { - throw new RuntimeException('Server licence public key is invalid.'); - } + // Derive the verification key from the same private seed used by LicenseSigner. + // Keeping a separate SONARDESK_LICENSE_PUBLIC_KEY allowed the two keys to drift: + // activation would sign a perfectly valid token with the new private key, then + // /validate would reject that same token using a stale public key. + $public = $this->publicKeyFromPrivateSeed(); if (! sodium_crypto_sign_verify_detached($signature, $payload, $public)) { throw new RuntimeException('Licence token signature is invalid.'); } @@ -26,6 +27,23 @@ final class LicenseTokenVerifier return is_array($decoded) ? $decoded : throw new RuntimeException('Licence token payload is invalid.'); } + private function publicKeyFromPrivateSeed(): string + { + $path = config('sonardesk.private_key_path'); + if (! $path || ! is_file($path)) { + throw new RuntimeException('SONARDESK_LICENSE_PRIVATE_KEY_PATH is not configured or does not exist.'); + } + + $seed = base64_decode(trim((string) file_get_contents($path)), true); + if ($seed === false || strlen($seed) !== SODIUM_CRYPTO_SIGN_SEEDBYTES) { + throw new RuntimeException('The SonarDesk private key must be a Base64-encoded 32-byte Ed25519 seed.'); + } + + $pair = sodium_crypto_sign_seed_keypair($seed); + + return sodium_crypto_sign_publickey($pair); + } + private function decode(string $value): string { $value = strtr($value, '-_', '+/'); diff --git a/deploy.php b/deploy.php index 628690d..dd96cd1 100755 --- a/deploy.php +++ b/deploy.php @@ -18,8 +18,8 @@ add('shared_dirs', ['storage']); add('writable_dirs', ['storage', 'bootstrap/cache']); // Host Definitions -host('production') - ->set('hostname', 'sonardsk.absdev.net') +host('main') + ->set('hostname', 'sonardesk.absdev.net') ->set('remote_user', 'mag') ->set('branch', 'main') ->set('deploy_path', '/var/www/sonardesk/main');