Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion app/Support/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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);
Expand Down
45 changes: 45 additions & 0 deletions tests/Unit/Support/HelpersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Log;
use phpseclib3\Crypt\EC\PrivateKey;
use phpseclib3\Crypt\PublicKeyLoader;

uses(RefreshDatabase::class);

beforeEach(function (): void {
$this->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);
}
});
Loading