Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Validating SSH keys on Laravel
Matheus Lopes Santos
Matheus Lopes Santos

Posted on

     

Validating SSH keys on Laravel

When we're called to develop an application, we should keep in mind that we might have to deal with various types of problems, some of which we may never have imagined facing. However, sometimes we need to step out of our comfort zone.

Understanding the Problem

A few days ago, I was tasked with building a feature that would receive a developer's public key and later send it to Laravel forge, allowing the user to have SSH access to the respective servers.

Awesome, Matheusão, how am I going to validate this type of data?

Initially, we think about validating the basics, such as the length of the string, whether it already exists in the database, etc:

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

Okay, but what if the user passes, I don't know, all the letters of the alphabet? Unfortunately, it will pass the validation 🙁.

In Search of the Perfect Validation

I did a lot of research on how to perform this validation. In many blogs, I saw many people recommending using native functions likeopenssl_verify,openssl_get_publickey, oropenssl_pkey_get_details, but unfortunately, they didn't work for what I needed (Remember, an SSH key is different from an SSL key, so these functions won't work). In other forums, I saw people suggesting using the packagehttps://phpseclib.com/. But think about it, why install a package when you're only going to use one class and one of its methods?

I see this as completely unnecessary coupling, but anyway...

Going a Bit Deeper

After some research, I found that we can usessh-keygen to validate this string for us, but how?

For this, we can use two flags,-l to get the fingerprint and-f to specify the file path. So our command would look like this:

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

And this way, we can check if our SSH key is valid or not.

Creating Our Validation Command

Laravel introduced a component calledProcess starting from version 10, which is nothing more than a wrapper around Symfony'sProcess component. It's with this little guy that we're going to work our magic.

Of course, we could use theexec function, a native PHP function. However, if you think you don't need to use this wrapper, feel free to do so 🙂👍🏻.

Let's think about what we need to do:

  • We need to receive the string containing the user's key.
  • We need to save this string somewhere accessible.
  • We need to call thessh-keygen command with the file path.
  • We need to delete the file after validation.

Setting Things Up

Let's create a directory insidestorage/app called ssh. Don't forget to exclude this new directory from your version control:

storage/app/.gitignore

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

storage/app/ssh/.gitignore

*!.gitignore
Enter fullscreen modeExit fullscreen mode

Writing Our Class

Now we can create our class that will interact withssh-keygen.

App/Terminal/ValidateSsh.php

<?phpdeclare(strict_types=1);namespaceApp\Terminal;useIlluminate\Support\Facades\Process;useIlluminate\Support\Str;classValidateSsh{privatestring$keyPath;publicfunction__construct(privatereadonlystring$content){$this->keyPath=storage_path('app/ssh/'.Str::uuid().'.pub');file_put_contents($this->keyPath,$this->content);}publicfunction__invoke():bool{returnProcess::run(command:'ssh-keygen -lf '.$this->keyPath.' && rm '.$this->keyPath,)->successful();}}
Enter fullscreen modeExit fullscreen mode

Great, our class is ready to be used.

  • It receives the content and saves it with a random name.
  • It checks the file, and if successful, it deletes it as well.

Now, let's write our tests.

tests/Unit/Terminal/ValidateSshTest.php

<?phpdeclare(strict_types=1);useApp\Terminal\ValidateSsh;it('should return true if process if file is valid',function(string$key){$validateSsh=newValidateSsh($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=newValidateSsh('a simple text file');expect($validateSsh())->toBeFalsy();});
Enter fullscreen modeExit fullscreen mode

Writing Our Rule

Think it's over? Not at all. The responsibility of theValidateSsh class is only to check if the key is valid or not.

Let's create a rule so that we can use this validation.

php artisan make:rule IsSshKeyValid
Enter fullscreen modeExit fullscreen mode

Great, now we can do the following:

<?phpdeclare(strict_types=1);namespaceApp\Rules;useApp\Terminal\ValidateSsh;useClosure;useIlluminate\Contracts\Validation\ValidationRule;useIlluminate\Translation\PotentiallyTranslatedString;classIsSshKeyValidimplementsValidationRule{/**     * @param Closure(string): PotentiallyTranslatedString $fail     */publicfunctionvalidate(string$attribute,mixed$value,Closure$fail):void{$validateSsh=newValidateSsh($value);if(!$validateSsh()){$fail('The :attribute is not a valid SSH key.');}}}
Enter fullscreen modeExit fullscreen mode

With this, we're ready to write our HTTP tests ❤️

Testing Our HTTP Call

Before moving on to the HTTP tests, we need to add our rule to our validation rules:

'ssh_key'=>['nullable','string','unique:users,ssh_key','max:5000',newIsSshKeyValid(),],
Enter fullscreen modeExit fullscreen mode

And our tests for this field can look like this:

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 modeExit fullscreen mode

Cool, right?

Now, I can register users in my system without worrying about those funny folks who might enter "aaaaaaa" in thessh_key field 😃.

And remember, sometimes we need to think outside the box to find solutions to some problems. The more open-minded we are, the faster we can make progress and learn new things.

Cheers, and until next time 😗 🧀

Top comments(3)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
justinkluever profile image
Justin K.
  • Location
    Germany, Schleswig Holstein
  • Joined

The fact that this validation creates a file on the server's filesystem and also runs a command doing something with said file is a bit dangerous if not done correctly tho, and i recommend using the Storage facade instead of raw file_put_contents as this would be more integrated into laravel to make use of its features.

Otherwise this a good and helpful article ^^

CollapseExpand
 
webwizo profile image
Asif Iqbal
  • Joined

Nice to read such code. I hope I will be able to translate this code in my personal project.

Awesome 👏

CollapseExpand
 
devlopez profile image
Matheus Lopes Santos
I'm a guy passionate about PHP and web development. I enjoy playing music, cycling, and watching some movies from time to time.
  • Work
    Tech Lead at DevSquad
  • Joined

Thank you my friend ❤️

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

I'm a guy passionate about PHP and web development. I enjoy playing music, cycling, and watching some movies from time to time.
  • Work
    Tech Lead at DevSquad
  • Joined

More fromMatheus Lopes Santos

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp