diff --git a/app/Support/helpers.php b/app/Support/helpers.php index 27f76d295..1fa412fb8 100755 --- a/app/Support/helpers.php +++ b/app/Support/helpers.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Route; use Illuminate\Support\ProcessUtils; use Symfony\Component\Process\PhpExecutableFinder; @@ -20,13 +21,24 @@ function generate_public_key(string $privateKeyPath, string $publicKeyPath): voi /** * ssh-keygen builds linked against LibreSSL (e.g. OpenSSH 10 on macOS) cannot write * ed25519 keys as PEM, so fall back to the OpenSSH format, which phpseclib also reads. + * + * @throws RuntimeException */ function generate_key_pair(string $path): void { exec("ssh-keygen -t ed25519 -m PEM -N '' -f ".escapeshellarg($path)); if (! file_exists($path)) { - exec("ssh-keygen -t ed25519 -N '' -f ".escapeshellarg($path)); + exec("ssh-keygen -t ed25519 -N '' -f ".escapeshellarg($path).' 2>&1', $output, $exitCode); + + if ($exitCode !== 0 || ! file_exists($path)) { + Log::error('Failed to generate SSH key pair', [ + 'exit_code' => $exitCode, + 'output' => implode("\n", $output), + ]); + + throw new RuntimeException('Failed to generate SSH key pair.'); + } } chmod($path, 0400); diff --git a/tests/Unit/Support/HelpersTest.php b/tests/Unit/Support/HelpersTest.php new file mode 100644 index 000000000..f34be58bf --- /dev/null +++ b/tests/Unit/Support/HelpersTest.php @@ -0,0 +1,45 @@ +keyPath = sys_get_temp_dir().'/vito-test-key-'.uniqid(); +}); + +afterEach(function (): void { + foreach ([$this->keyPath, $this->keyPath.'.pub'] as $path) { + if (file_exists($path)) { + unlink($path); + } + } +}); + +it('generates a key pair the ssh helper can load', function (): void { + generate_key_pair($this->keyPath); + + expect($this->keyPath)->toBeReadableFile() + ->and($this->keyPath.'.pub')->toBeReadableFile() + ->and(fileperms($this->keyPath) & 0777)->toBe(0400) + ->and(PublicKeyLoader::loadPrivateKey((string) file_get_contents($this->keyPath))) + ->toBeInstanceOf(PrivateKey::class); +}); + +it('fails loudly without leaking the key path when generation fails', function (): void { + Log::shouldReceive('error')->once(); + + $path = sys_get_temp_dir().'/vito-missing-'.uniqid().'/key'; + + try { + generate_key_pair($path); + $this->fail('Expected generate_key_pair() to throw.'); + } catch (RuntimeException $e) { + expect($e->getMessage()) + ->toBe('Failed to generate SSH key pair.') + ->not->toContain($path); + } +});