Skip to content

Instantly share code, notes, and snippets.

@Saruspete
Created February 5, 2020 13:54
Show Gist options
  • Save Saruspete/d0348b2a84289a5c9f635e5e2ea0e320 to your computer and use it in GitHub Desktop.
Save Saruspete/d0348b2a84289a5c9f635e5e2ea0e320 to your computer and use it in GitHub Desktop.
check ssh public key format
#!/usr/bin/perl
# #################################################################
# SSH Public key validator
#
# Check the structural validity of a public SSH key
# #################################################################
#
# Author: Adrien Mahieux <[email protected]>
# Version: 1.0
#
# information found on :
# http://www.perlmonks.org/?node_id=775820
# http://comp.security.ssh.narkive.com/WNJrPgJD/public-key-storage-format-in-openssh
# http://tools.ietf.org/html/rfc4253#section-6.6
# http://tools.ietf.org/html/rfc4716
# http://blogs.gnome.org/ovitters/2008/06/02/ssh-public-keys/
#
# #################################################################
use strict;
use warnings;
use Data::Dumper;
use MIME::Base64;
use Digest::MD5 qw(md5_hex);
use Getopt::Long qw(:config no_ignore_case gnu_compat bundling permute auto_help);
use Pod::Usage;
use bytes;
# Return code
my $return = 0;
my $err_files = '';
# Options vars
my $opt_help = 0;
my $opt_verbose = 0; # Should we be verbose
my $opt_fix = 0; # should we fix the errors
my $opt_suffix = '.clean'; # suffix
my @opt_files; # List of files to process
Getopt::Long::Configure('pass_through');
GetOptions(
'help|h|?' => \$opt_help,
'verbose|v+' => \$opt_verbose,
'fix|f' => \$opt_fix,
'suffix|s=s' => \$opt_suffix,
'<>' => \&listadd_file,
);
# ##################################################################
# Main stuff...
# ##################################################################
pod2usage(-verbose => 2) if ($opt_help && $opt_verbose);
pod2usage(-verbose => 1) if ($opt_help);
# Input check
if (!@opt_files) {
print "Error: no file specified. See $0 --help\n";
exit 1;
}
# Files validity check
if ($err_files) {
print "Error: invalid file(s): \n", $err_files;
exit 1;
}
# Do the real job
foreach my $keyfile (@opt_files) {
$return += process_file($keyfile);
}
# And exit code is the number of lines failed
exit $return;
# ##################################################################
# Processing subs
# ##################################################################
sub listadd_file {
my $file = $_[0];
# Getopt object
if (ref($file) && UNIVERSAL::can($file, 'can')) {
$file = $file->{name};
}
if (!-e $file) {
$err_files .= "$file\n";
return 0;
}
push(@opt_files, $file);
return 1;
}
sub process_file {
# Input file
my ($file_in) = @_;
# Cleaned data container
my $out_data = '';
my $errors = 0;
my %chk_datahash;
# Read the file for the data
open(FIN, $file_in);
my $nline = 0;
READ_LOOP:
while (my $line = <FIN>) {
$nline++;
$line = normalize_spaces($line);
# Empty line
if ($line =~ m/^$/) {
next READ_LOOP;
}
# Seek the data
my ($keytype, $datastr) = ($line =~ m/(ssh-(?:dss|rsa)) ([^\s]+)/);
# If we didn't succeed in the struct extraction
if (!$keytype || $keytype eq '' ||
!$datastr || $datastr eq '') {
$opt_verbose && print "line $nline: invalid SSH-Key data: $line\n";
$errors++;
next READ_LOOP;
}
# Base64 validity structure
if (! check_base64($datastr, $nline) ) {
$opt_verbose && print "line $nline: base64 integrity check failed: $line\n";
$errors++;
next READ_LOOP;
}
# Check the data struct
if (! check_keycontent($datastr, $keytype, $nline) ) {
$opt_verbose && print "line $nline: SSH Content check failed: $line\n";
$errors++;
next READ_LOOP;
}
# Duplicate of the key in the file
my $datahash = md5_hex($datastr);
if ($chk_datahash{$datahash}) {
$opt_verbose && print "line $nline: Duplicate key on line $chk_datahash{$datahash}: $line\n";
$errors++;
next READ_LOOP;
}
# ALL Checks passed ! Valid key
# store the hash
$chk_datahash{$datahash} = $nline;
# store the data
$out_data .= $line."\n";
}
# Should we create a fixed file ?
if ($opt_fix > 0) {
my $file_out = "${file_in}${opt_suffix}";
open(FOUT, ">", $file_out);
print FOUT $out_data;
close(FOUT);
}
return $errors;
}
# #################################################################
# Check subs
# #################################################################
# Check the Base64 string validity
sub check_base64 {
my ($datastr) = @_;
# Regex from http://www.perlmonks.org/?node_id=775820
return $datastr =~ m{
^
(?: [A-Za-z0-9+/]{4} )*
(?:
[A-Za-z0-9+/]{2} [AEIMQUYcgkosw048] =
|
[A-Za-z0-9+/] [AQgw] ==
)?
\z
}x;
}
# Check the SSH Key content (advanced check)
sub check_keycontent {
my ($datastr, $keytype, $nline) = @_;
# And parse the data
my $dataraw = decode_base64($datastr);
my $offset = 0;
my $valid = 1;
my $loops = 1;
# Loop for strings extraction
for (my $loop = 0; $valid && $loop < $loops; $loop++) {
# Extract the string length and data
my $str_size = unpack('@'.$offset.'N', $dataraw);
$offset += 4;
my $str_data = unpack('@'.$offset.'a['.$str_size.']', $dataraw);
$offset += $str_size;
# If the unpacked data is not valid (truncated)
if ($str_size == 0 || $str_size != bytes::length($str_data)) {
$opt_verbose && print "line $nline: loop $loop: wanted $str_size, got ".bytes::length($str_data),"\n";
$valid = 0;
}
# If it's the first loop check also the type
if ($loop == 0) {
# ssh-DSA data is composed of 4 bignums
if ($str_data eq 'ssh-dss') {
$loops += 4;
}
# ssh-RSA data is only composed of 2 bignums
elsif ($str_data eq 'ssh-rsa') {
$loops += 2;
}
# Check the str keytype
if ($keytype ne $str_data) {
$opt_verbose && printf "line %s: Key type is %s while its %.7s\n", $nline, $keytype, $str_data;
$valid = 0;
}
}
}
return $valid;
}
sub normalize_spaces {
my ($str) = @_;
return '' if (!$str);
# Trim the spaces
$str =~ s/(^\s*)|(\s*$)//;
# Remove the newlines
chomp($str);
# Change the tabs
$str =~ s/\t/ /g;
# Supress the multiple spaces
$str =~ s/\s+/ /g;
return $str;
}
__END__
=head1 NAME
check_sshkey.pl - SSH Public key validation script
=head1 SYNOPSIS
check_sshkey.pl [options] <file> [file...]
Options:
-h, --help this help
-v, --verbose increase verbosity level
-f, --fix Fix the bad files (remove bad lines)
-s, --suffix Suffix for the cleaned files (default: .clean)
=head1 DESCRIPTION
=head2 USAGE
=head1 BUGS
Tickets on http://dev.mahieux.net/
=head1 AUTHOR
Adrien Mahieux <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment