124 lines
5.1 KiB
PHP
124 lines
5.1 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Admin\ConfirmGenerateSshKey;
|
|
use App\Livewire\Admin\Integrations;
|
|
use App\Services\Secrets\SecretVault;
|
|
use App\Services\Ssh\Keypair;
|
|
use App\Support\ProvisioningSettings;
|
|
use Livewire\Livewire;
|
|
use phpseclib3\Crypt\EC;
|
|
use phpseclib3\Crypt\PublicKeyLoader;
|
|
|
|
/**
|
|
* Der Tresor hielt `ssh.private_key` write-only und hatte keinen Erzeuger: das
|
|
* Paar musste in einer Shell entstehen und von Hand hineinkopiert werden. Eine
|
|
* Bereitschaftsseite, die etwas bemängelt, das nur eine Shell beheben kann,
|
|
* hat die Arbeit verschoben statt erledigt.
|
|
*/
|
|
beforeEach(function () {
|
|
config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)));
|
|
});
|
|
|
|
it('mints a key the SSH client itself can load', function () {
|
|
// Die eigentliche Zusicherung, und der Grund für phpseclib statt eines
|
|
// selbstgeschriebenen OpenSSH-Rahmens: PhpseclibRemoteShell::connectWithKey()
|
|
// reicht den gespeicherten Text an genau dieses PublicKeyLoader::load()
|
|
// weiter. Was diese Prüfung lädt, ist damit dasselbe, was sich später an
|
|
// einem Host anmeldet — ein Format, das nur „nach OpenSSH aussieht", fiele
|
|
// erst bei der ersten Host-Übernahme auf.
|
|
$pair = Keypair::generate();
|
|
|
|
$loaded = PublicKeyLoader::load($pair->privateKey);
|
|
|
|
expect($loaded)->toBeInstanceOf(EC\PrivateKey::class)
|
|
->and($loaded->getCurve())->toBe('Ed25519');
|
|
});
|
|
|
|
it('publishes a public half that is one authorized_keys line and belongs to the private half', function () {
|
|
// EstablishSshTrust hängt genau diesen String mit `echo … >> authorized_keys`
|
|
// an. Ein Zeilenumbruch darin schriebe zwei kaputte Zeilen; ein öffentlicher
|
|
// Teil, der nicht zum privaten gehört, ergäbe eine Übernahme, die beim
|
|
// Schlüssel-Login scheitert, nachdem das Passwort schon verworfen wurde.
|
|
$pair = Keypair::generate();
|
|
|
|
expect($pair->publicKey)->toStartWith('ssh-ed25519 ')
|
|
->and($pair->publicKey)->not->toContain("\n")
|
|
->and(PublicKeyLoader::load($pair->privateKey)->getPublicKey()->toString('OpenSSH', ['comment' => 'clupilot']))
|
|
->toBe($pair->publicKey);
|
|
});
|
|
|
|
it('never mints the same key twice', function () {
|
|
expect(Keypair::generate()->privateKey)->not->toBe(Keypair::generate()->privateKey);
|
|
});
|
|
|
|
it('stores the private half in the vault and publishes only the public half', function () {
|
|
$owner = operator('Owner');
|
|
|
|
$page = Livewire::actingAs($owner, 'operator')
|
|
->test(Integrations::class)
|
|
->set('confirmablePassword', 'password')
|
|
->call('confirmPassword')
|
|
->call('generateSshKey')
|
|
->assertHasNoErrors();
|
|
|
|
$private = (string) app(SecretVault::class)->get('ssh.private_key');
|
|
$public = ProvisioningSettings::sshPublicKey();
|
|
|
|
expect($private)->toContain('BEGIN OPENSSH PRIVATE KEY')
|
|
->and($public)->toStartWith('ssh-ed25519 ')
|
|
// Das Feld auf der Seite zeigt den öffentlichen Teil, damit der
|
|
// Betreiber ihn ohne Umweg auf einen Host legen kann.
|
|
->and($page->get('sshPublicKey'))->toBe($public)
|
|
->and(PublicKeyLoader::load($private)->getPublicKey()->toString('OpenSSH', ['comment' => 'clupilot']))
|
|
->toBe($public);
|
|
});
|
|
|
|
it('never lets the generated private half reach the page', function () {
|
|
// Derselbe Maßstab wie für jeden anderen Tresorwert: gespeichert wird er,
|
|
// angezeigt nie. Ein erzeugter Schlüssel ist dabei der heikelste Fall — er
|
|
// entsteht in derselben Anfrage, die die Antwort schreibt.
|
|
$owner = operator('Owner');
|
|
|
|
$page = Livewire::actingAs($owner, 'operator')
|
|
->test(Integrations::class)
|
|
->set('confirmablePassword', 'password')
|
|
->call('confirmPassword')
|
|
->call('generateSshKey');
|
|
|
|
$private = (string) app(SecretVault::class)->get('ssh.private_key');
|
|
$body = json_encode($page->snapshot).$page->html();
|
|
|
|
expect($body)->not->toContain('BEGIN OPENSSH PRIVATE KEY');
|
|
|
|
foreach (array_slice(explode("\n", trim($private)), 1, -1) as $zeile) {
|
|
expect($body)->not->toContain(trim($zeile));
|
|
}
|
|
});
|
|
|
|
it('refuses to generate without a confirmed password', function () {
|
|
// Dieselbe Sperre wie Speichern und Vergessen: das Erzeugen ÜBERSCHREIBT
|
|
// den Schlüssel, mit dem diese Anwendung jeden Host erreicht.
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(Integrations::class)
|
|
->call('generateSshKey')
|
|
->assertForbidden();
|
|
});
|
|
|
|
it('refuses to generate without the capability', function () {
|
|
Livewire::actingAs(operator('Admin'), 'operator')
|
|
->test(Integrations::class)
|
|
->call('generateSshKey')
|
|
->assertForbidden();
|
|
});
|
|
|
|
it('confirms in a modal before replacing the identity every host trusts', function () {
|
|
// R23, und hier mit echten Folgen: der bisherige öffentliche Teil bleibt in
|
|
// den authorized_keys jedes übernommenen Hosts stehen, der neue steht dort
|
|
// noch nicht. Zwischen Erzeugen und Ausrollen erreicht die Konsole keine
|
|
// einzige Maschine mehr.
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(ConfirmGenerateSshKey::class)
|
|
->call('confirm')
|
|
->assertDispatched('ssh-key-generate-confirmed');
|
|
});
|