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']
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
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/
storage/app/ssh/.gitignore
*
!.gitignore
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();
}
}
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();
});
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
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.');
}
}
}
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(),
],
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);
});
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)
Conteúdo de Top Demais, Parabéns meu querido
Valeu meu nobre ❤️
Boa Matheusão, conteúdo muito top
Obrigado meu nobre ❤️