CluPilotCloud/app/Services/Ssh/Keypair.php

53 lines
2.1 KiB
PHP

<?php
namespace App\Services\Ssh;
use phpseclib3\Crypt\EC;
/**
* The SSH identity this installation reaches its hosts with, minted in PHP.
*
* Same reasoning as App\Services\Wireguard\Keypair, one layer up: the console
* runs as www-data inside a container, shelling out to `ssh-keygen` would need
* a binary and a writable path that only exist by accident, and a key written
* to a temp file is a private key on a disk somebody has to remember to shred.
* Generated here, the private half goes straight into the vault and never
* touches the filesystem at all.
*
* Built on phpseclib rather than a hand-rolled `openssh-key-v1` frame, and not
* for convenience: PhpseclibRemoteShell::connectWithKey() hands the stored text
* to `PublicKeyLoader::load()`. Minting with the same library is what makes
* "this is a valid OpenSSH key" the same statement as "this logs in" — a
* self-written frame that merely looked right would be found out at the first
* host takeover, after the root password had already been scrubbed.
*
* Ed25519 because a modern OpenSSH accepts it everywhere, the key is short
* enough to read out over a phone, and there is no key size to get wrong.
*/
final readonly class Keypair
{
/**
* What the public half is labelled with in `authorized_keys`.
*
* Fixed rather than derived from the installation: this string lands on
* every host's root account, and an operator reading that file wants to see
* WHO the key belongs to — not the hostname of whichever machine the panel
* happened to run on when it was minted.
*/
public const COMMENT = 'clupilot';
public function __construct(public string $privateKey, public string $publicKey) {}
public static function generate(): self
{
$key = EC::createKey('Ed25519');
return new self(
privateKey: $key->toString('OpenSSH'),
// One line, `ssh-ed25519 AAAA… clupilot`, which is the whole of
// what EstablishSshTrust appends to a host's authorized_keys.
publicKey: $key->getPublicKey()->toString('OpenSSH', ['comment' => self::COMMENT]),
);
}
}