DEV Community

Cover image for Validando chave ssh pública no Laravel
Matheus Lopes Santos
Matheus Lopes Santos

Posted on

Validando chave ssh pública no Laravel

Quando somos chamados para desenvolver uma aplicação, devemos ter em mente que podemos ter que lidar com vários tipos de problemas, aqueles que talvez jamais tenhamos imaginado em enfrentar. Contudo, vez ou outra precisamos sair de nossa zona de conforto.

Entendendo o problema

Há alguns dias, fui acionado para construir uma feature que iria receber a chave pública de um dev e iria enviá-la, posteriormente, para o forge, fazendo com que o usuário tivesse acesso ssh aos devidos servidores.

Maravilha Matheusão, como vou validar essa tipo de dado?

Incialmente a gente pensa em validar o básico, como o tamanho da string, se ela já existe no banco de dados, etc:

'ssh_key' => ['nullable', 'string', 'unique:users,ssh_key', 'max:5000']
Enter fullscreen mode Exit fullscreen mode

Beleza, mas e se o meu usuário passar, sei lá, todas as letras do alfabeto? Infelizmente vai passar pela validação 🙁.

À procura da validação perfeita

Pesquisei bastante à respeito de como realizar essa validação. Em vários blogs, vi muita gente indicando usar funções nativas, como o openssl_verify, openssl_get_publickey ou a openssl_pkey_get_details, mas elas infelizmente não funcionaram para o que eu precisava (Lembre-se, uma chave SSH é diferente de uma chave SSL, por isso essas funções não funcionam). Vi em outros fóruns um pessoal dizendo para utilizar o package https://phpseclib.com/. Mas para pra pensar, pra que instalar um pacote que você só vai utilizar uma classe e somente um método dela?

Eu vejo isso como um acoplamento totalmente desnecessário, mas enfim…

Indo um pouco mais fundo

Depois de alguma pesquisa, vi que podemos usar o ssk-keygen para validar essa string pra gente, mas como?

Para isso, podemos usar duas flags, a -l para pegar o fingerprint e o -f para indicar o caminho do arquivo. Então o nosso comando ficaria assim:

ssh-keygen -lf /path/to/my/file.pub
Enter fullscreen mode Exit fullscreen mode

E dessa forma poderemos checar se nossa chave SSH é válida ou não.

Criando nosso comando de checagem

O Laravel trouxe, a partir da versão 10, um componente chamado Process, que nada mais é do que um wrapper ao redor do componente Process do Symfony. É com esse carinha que vamos fazer a mágica.

Logicamente que poderíamos usa a função exec, nativa do php. Porém, se você acha que não é necessário usar este wrapper, fique à vontade 🙂👍🏻.

Vamos pensar o que precisamos fazer:

  • Preciso receber a string contendo a chave do usuário.
  • Preciso salvar essa string em algum lugar acessível.
  • Preciso chamar o comando ssh-keygen com o caminho do arquivo.
  • Preciso destruir o arquivo após a validação.

Configurando as Coisas

Vamos criar um diretório dentro de storage/app chamado ssh. Não se esqueça de remover esse novo diretório do seu versionamento:

storage/app/.gitignore

*
!public/
!.gitignore
!ssh/
Enter fullscreen mode Exit fullscreen mode

storage/app/ssh/.gitignore

*
!.gitignore
Enter fullscreen mode Exit fullscreen mode

Escrevendo nossa classe

Agora podemos criar a nossa classe que fará a interação com o ssh-keygen

App/Terminal/ValidateSsh.php

<?php

declare(strict_types=1);

namespace App\Terminal;

use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;

class ValidateSsh
{
    private string $keyPath;

    public function __construct(
        private readonly string $content
    ) {
        $this->keyPath = storage_path('app/ssh/' . Str::uuid() . '.pub');

        file_put_contents($this->keyPath, $this->content);
    }

    public function __invoke(): bool
    {
        return Process::run(
            command: 'ssh-keygen -lf ' . $this->keyPath . ' && rm ' . $this->keyPath,
        )->successful();
    }
}
Enter fullscreen mode Exit fullscreen mode

Maravilha, a nossa classe está prontinha pra ser utilizada.

  • Recebe o conteúdo e salva com um nome aleatório.
  • Checa o arquivo e em caso de sucesso, já o apaga também.

Agora, vamos escrever os nossos testes.

tests/Unit/Terminal/ValidateSshTest.php

<?php

declare(strict_types=1);

use App\Terminal\ValidateSsh;

it('should return true if process if file is valid', function (string $key) {
    $validateSsh = new ValidateSsh($key);

    expect($validateSsh())->toBeTruthy();
})->with([
    'RSA'   => 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQD3vYKSuh7rJf+NtWn04CFyT9+nmx+i+/sP+yMN9ueJ+Rd5Ku6d9kgscK2xwlRlkcA0sethslu0WUsG81RC1lVpF6iLrc/9O45ZhEY1CB/7dofr+7ZNwu/DJtbW6YE7oyT5G97BUW763TMq/YO9/xjMToetElTEJ4hUVWdP8q93b3MVHBazk2PEuS05wzP4p5XeQnhKq4LISetJFEgI8Y+HEpK29GiU/18fhaGZvdVwOToOxTwEwBbS3fTLNkBaUTWw9q3i7S60RRncBCHppcs2irrzw7yt7ZQOnut/BIjIGESoxx+N4ZrpTmX6P5d3/9Duk40Mfwh1ftsvze6o5AW4Xi0tki8b6bsMXmO7SapqVdiMZ5/4BWOkqHWhi926qz7I9NWoZuVFAUpSoe6fObzQBRooVp7ARw7gJ4C+Q4xc1gJJkZoQ/Wj/wHkVnbLw9M5+t5GjyWgDDOr5iyoGOyIwhuEFvATzIYH0z5B6anL1n6XQmeGh5OWKJN8wE5qVNTU= worker@envoyer.io',
    'EDCSA' => 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFvXWSVYzRnjxYsz/xKjOjAaPjzg98MMHaDulQYczTX28xlsMmFkviCeCCv7CLh19ydoH4LNKpvgTGiMXz8ib68= worker@envoyer.',
]);

it('should return false if ssh file is invalid', function () {
    $validateSsh = new ValidateSsh('a simple text file');

    expect($validateSsh())->toBeFalsy();
});
Enter fullscreen mode Exit fullscreen mode

Escrevendo nossa rule

Pensa que acabou? Não mesmo. A responsabilidade da classe ValidateSsh é apenas a de verificar se a chave é válida ou não.

Vamos criar uma rule para que possamos fazer uso dessa validação.

php artisan make:rule IsSshKeyValid
Enter fullscreen mode Exit fullscreen mode

Maravilha, agora, podemos fazer o seguinte:

<?php

declare(strict_types=1);

namespace App\Rules;

use App\Terminal\ValidateSsh;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Translation\PotentiallyTranslatedString;

class IsSshKeyValid implements ValidationRule
{
    /**
     * @param Closure(string): PotentiallyTranslatedString $fail
     */
    public function validate(
        string $attribute,
        mixed $value,
        Closure $fail
    ): void {
        $validateSsh = new ValidateSsh($value);

        if (!$validateSsh()) {
            $fail('The :attribute is not a valid SSH key.');
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Com isso, já estamos prontos para fazer os nossos testes http ❤️

Testando nosso nossa chamada Http

Antes de prosseguir para os testes http, precisamos adicionar a nossa rule em nossas regras de validação:

'ssh_key' => [
    'nullable',
    'string',
    'unique:users,ssh_key',
    'max:5000',
    new IsSshKeyValid(),
],
Enter fullscreen mode Exit fullscreen mode

E nossos testes para esse campo podem ficar dessa maneira:

it('should validate `ssh_key` field', function (mixed $value, string $error) {
    login();

    postJson(route('api.users.store'), ['ssh_key' => $value])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['ssh_key' => $error]);
})->with([
    fn () => [5000, __('validation.string', ['attribute' => 'ssh key'])],
    fn () => [str_repeat('a', 5001), __('validation.max.string', ['attribute' => 'ssh key', 'max' => 5000])],
    fn () => ['aa', 'The ssh key is not a valid SSH key.'],
    function () {
        $user = User::factory()
            ->create([
                'ssh_key' => 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQD3vYKSuh7rJf+NtWn04CFyT9+nmx+i+/sP+yMN9ueJ+Rd5Ku6d9kgscK2xwlRlkcA0sethslu0WUsG81RC1lVpF6iLrc/9O45ZhEY1CB/7dofr+7ZNwu/DJtbW6YE7oyT5G97BUW763TMq/YO9/xjMToetElTEJ4hUVWdP8q93b3MVHBazk2PEuS05wzP4p5XeQnhKq4LISetJFEgI8Y+HEpK29GiU/18fhaGZvdVwOToOxTwEwBbS3fTLNkBaUTWw9q3i7S60RRncBCHppcs2irrzw7yt7ZQOnut/BIjIGESoxx+N4ZrpTmX6P5d3/9Duk40Mfwh1ftsvze6o5AW4Xi0tki8b6bsMXmO7SapqVdiMZ5/4BWOkqHWhi926qz7I9NWoZuVFAUpSoe6fObzQBRooVp7ARw7gJ4C+Q4xc1gJJkZoQ/Wj/wHkVnbLw9M5+t5GjyWgDDOr5iyoGOyIwhuEFvATzIYH0z5B6anL1n6XQmeGh5OWKJN8wE5qVNTU= worker@envoyer.io',
            ]);

        return [$user->ssh_key, __('validation.unique', ['attribute' => 'ssh key'])];
    },
]);

it('should store an user', function () {
    login();

    $data = [
        'name'    => 'Matheus Santos',
        'email'   => 'matheusao@my-company.com',
        'ssh_key' => 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQD3vYKSuh7rJf+NtWn04CFyT9+nmx+i+/sP+yMN9ueJ+Rd5Ku6d9kgscK2xwlRlkcA0sethslu0WUsG81RC1lVpF6iLrc/9O45ZhEY1CB/7dofr+7ZNwu/DJtbW6YE7oyT5G97BUW763TMq/YO9/xjMToetElTEJ4hUVWdP8q93b3MVHBazk2PEuS05wzP4p5XeQnhKq4LISetJFEgI8Y+HEpK29GiU/18fhaGZvdVwOToOxTwEwBbS3fTLNkBaUTWw9q3i7S60RRncBCHppcs2irrzw7yt7ZQOnut/BIjIGESoxx+N4ZrpTmX6P5d3/9Duk40Mfwh1ftsvze6o5AW4Xi0tki8b6bsMXmO7SapqVdiMZ5/4BWOkqHWhi926qz7I9NWoZuVFAUpSoe6fObzQBRooVp7ARw7gJ4C+Q4xc1gJJkZoQ/Wj/wHkVnbLw9M5+t5GjyWgDDOr5iyoGOyIwhuEFvATzIYH0z5B6anL1n6XQmeGh5OWKJN8wE5qVNTU= worker@envoyer.io',
    ];

    postJson(route('api.users.store'), $data)
        ->assertCreated();

    assertDatabaseHas(Users::class, $data);
});
Enter fullscreen mode Exit fullscreen mode

Legal não?

Agora, posso cadastrar os usuários no meu sistema sem me preocupar com aqueles engraçadinhos que vão passar um aaaaaaaa no campo ssh_key 😃.

E lembre-se, às vezes precisamos sair do óbvio para encontrar as soluções para alguns problemas. Quanto mais mente-aberta formos, mais rápido conseguimos progredir e aprender novas coisas.

Um abraço e até a próxima 😗 🧀

Top comments (4)

Collapse
 
medanielsantos profile image
Daniel Henrique

Conteúdo de Top Demais, Parabéns meu querido

Collapse
 
devlopez profile image
Matheus Lopes Santos

Valeu meu nobre ❤️

Collapse
 
z4nder profile image
Alexandre

Boa Matheusão, conteúdo muito top

Collapse
 
devlopez profile image
Matheus Lopes Santos

Obrigado meu nobre ❤️