Created
February 5, 2020 13:54
-
-
Save Saruspete/d0348b2a84289a5c9f635e5e2ea0e320 to your computer and use it in GitHub Desktop.
check ssh public key format
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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