| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | # -*- Perl -*- | 
| 2 |  |  |  |  |  |  | # | 
| 3 |  |  |  |  |  |  | # utility function to parse SSH public keys with | 
| 4 |  |  |  |  |  |  | # | 
| 5 |  |  |  |  |  |  | # run perldoc(1) on this file for documentation | 
| 6 |  |  |  |  |  |  |  | 
| 7 |  |  |  |  |  |  | package Data::SSHPubkey; | 
| 8 |  |  |  |  |  |  |  | 
| 9 | 3 |  |  | 3 |  | 56695 | use 5.010; | 
|  | 3 |  |  |  |  | 18 |  | 
| 10 | 3 |  |  | 3 |  | 15 | use strict; | 
|  | 3 |  |  |  |  | 4 |  | 
|  | 3 |  |  |  |  | 99 |  | 
| 11 | 3 |  |  | 3 |  | 16 | use warnings; | 
|  | 3 |  |  |  |  | 5 |  | 
|  | 3 |  |  |  |  | 97 |  | 
| 12 |  |  |  |  |  |  |  | 
| 13 | 3 |  |  | 3 |  | 15 | use Carp qw(croak); | 
|  | 3 |  |  |  |  | 3 |  | 
|  | 3 |  |  |  |  | 190 |  | 
| 14 | 3 |  |  | 3 |  | 1897 | use File::Temp (); | 
|  | 3 |  |  |  |  | 58750 |  | 
|  | 3 |  |  |  |  | 2028 |  | 
| 15 |  |  |  |  |  |  |  | 
| 16 |  |  |  |  |  |  | our ( $max_keys, $max_lines, %ssh_pubkey_types ); | 
| 17 |  |  |  |  |  |  |  | 
| 18 |  |  |  |  |  |  | require Exporter; | 
| 19 |  |  |  |  |  |  | our @ISA       = qw(Exporter); | 
| 20 |  |  |  |  |  |  | our @EXPORT_OK = qw(&convert_pubkeys &pubkeys %ssh_pubkey_types); | 
| 21 |  |  |  |  |  |  |  | 
| 22 |  |  |  |  |  |  | our $VERSION = '0.07'; | 
| 23 |  |  |  |  |  |  |  | 
| 24 |  |  |  |  |  |  | # rsa or ecdsa or ed25519 with the upper case forms presumably some | 
| 25 |  |  |  |  |  |  | # other encoding of one of these, so set very low by default | 
| 26 |  |  |  |  |  |  | $max_keys = 3; | 
| 27 |  |  |  |  |  |  |  | 
| 28 |  |  |  |  |  |  | # a 4096-bit RSA key is 16 lines in RFC4716, though this may need to be | 
| 29 |  |  |  |  |  |  | # set higher if you allow long comments, or | 
| 30 |  |  |  |  |  |  | $max_lines = 100; | 
| 31 |  |  |  |  |  |  |  | 
| 32 |  |  |  |  |  |  | @ssh_pubkey_types{qw(ecdsa ed25519 rsa PEM PKCS8 RFC4716)} = (); | 
| 33 |  |  |  |  |  |  | # NOTE these are taken from the ssh-keygen(1) -t or -m options which | 
| 34 |  |  |  |  |  |  | # differ from the strings present in the SSH key data | 
| 35 |  |  |  |  |  |  | # | 
| 36 |  |  |  |  |  |  | #   type        public key prefix | 
| 37 |  |  |  |  |  |  | #   ----------------------------------- | 
| 38 |  |  |  |  |  |  | #   ecdsa       ecdsa-sha2-nistp256 ... | 
| 39 |  |  |  |  |  |  | #   ed25519     ssh-ed25519 ... | 
| 40 |  |  |  |  |  |  | #   rsa         ssh-rsa ... | 
| 41 |  |  |  |  |  |  | # | 
| 42 |  |  |  |  |  |  | # those responsible for the confusion between these two different bits | 
| 43 |  |  |  |  |  |  | # of data in versions of this module prior to 0.05 have been sacked | 
| 44 |  |  |  |  |  |  |  | 
| 45 |  |  |  |  |  |  | sub convert_pubkeys { | 
| 46 | 1 |  |  | 1 | 1 | 3617 | my ($list) = @_; | 
| 47 | 1 |  |  |  |  | 2 | my @pubkeys; | 
| 48 | 1 |  |  |  |  | 4 | for my $ref (@$list) { | 
| 49 | 3 | 50 |  |  |  | 804 | if ( $ref->[0] =~ m/^(?:PEM|PKCS8|RFC4716)$/ ) { | 
|  |  | 0 |  |  |  |  |  | 
| 50 |  |  |  |  |  |  | # TODO perl (or CPAN module) conversion of these so don't | 
| 51 |  |  |  |  |  |  | # need to call out to this ssh-keygen which is not portable | 
| 52 |  |  |  |  |  |  | # to olden versions of ssh-keygen | 
| 53 | 3 |  |  |  |  | 51 | my $tmp = File::Temp->new; | 
| 54 | 3 |  |  |  |  | 1957 | print $tmp $ref->[1]; | 
| 55 | 3 |  |  |  |  | 15 | my $tfile = $tmp->filename; | 
| 56 | 3 | 50 |  |  |  | 6750 | open my $fh, '-|', qw(ssh-keygen -i -m), $ref->[0], '-f', $tfile | 
| 57 |  |  |  |  |  |  | or die "could not exec ssh-keygen: $!"; | 
| 58 | 3 |  |  |  |  | 38 | binmode $fh; | 
| 59 | 3 |  |  |  |  | 46 | push @pubkeys, do { local $/; readline $fh }; | 
|  | 3 |  |  |  |  | 70 |  | 
|  | 3 |  |  |  |  | 5898 |  | 
| 60 | 3 | 50 |  |  |  | 288 | close $fh or die "ssh-keygen failed with exit status $?"; | 
| 61 |  |  |  |  |  |  | } elsif ( $ref->[0] =~ m/^(?:ecdsa|ed25519|rsa)$/ ) { | 
| 62 | 0 |  |  |  |  | 0 | push @pubkeys, $ref->[1]; | 
| 63 |  |  |  |  |  |  | } else { | 
| 64 | 0 |  |  |  |  | 0 | croak 'unknown public key type ' . $ref->[0]; | 
| 65 |  |  |  |  |  |  | } | 
| 66 |  |  |  |  |  |  | } | 
| 67 | 1 |  |  |  |  | 374 | chomp @pubkeys; | 
| 68 | 1 |  |  |  |  | 18 | return \@pubkeys; | 
| 69 |  |  |  |  |  |  | } | 
| 70 |  |  |  |  |  |  |  | 
| 71 |  |  |  |  |  |  | sub pubkeys { | 
| 72 | 15 |  |  | 15 | 1 | 17480 | my ($input) = @_; | 
| 73 | 15 | 100 |  |  |  | 69 | croak "input must be string, GLOB, or scalar ref" if !defined $input; | 
| 74 | 14 |  |  |  |  | 21 | my $fh; | 
| 75 | 14 | 100 |  |  |  | 45 | if ( ref $input eq 'GLOB' ) { | 
| 76 | 1 |  |  |  |  | 7 | $fh = $input; | 
| 77 |  |  |  |  |  |  | } else { | 
| 78 | 13 | 50 |  | 2 |  | 379 | open $fh, '<', $input or croak "could not open $input: $!"; | 
|  | 2 |  |  |  |  | 12 |  | 
|  | 2 |  |  |  |  | 4 |  | 
|  | 2 |  |  |  |  | 13 |  | 
| 79 | 13 |  |  |  |  | 1438 | binmode $fh; | 
| 80 |  |  |  |  |  |  | } | 
| 81 | 14 |  |  |  |  | 27 | my @keys; | 
| 82 | 14 |  |  |  |  | 217 | while ( my $line = readline $fh ) { | 
| 83 | 24 | 50 |  |  |  | 78 | croak "too many input lines" if $. > $max_lines; | 
| 84 | 24 | 100 |  |  |  | 168 | if ( $line =~ m{^(-----BEGIN RSA PUBLIC KEY-----)} ) { | 
|  |  | 100 |  |  |  |  |  | 
|  |  | 100 |  |  |  |  |  | 
|  |  | 100 |  |  |  |  |  | 
| 85 | 3 |  |  |  |  | 34 | my $key = $1; | 
| 86 | 3 |  |  |  |  | 20 | my ( $ok, $data ) = _until_end( $fh, '-----END RSA PUBLIC KEY-----' ); | 
| 87 | 3 | 50 |  |  |  | 14 | croak "could not parse PEM pubkey: $data" unless defined $ok; | 
| 88 | 3 |  |  |  |  | 29 | push @keys, [ 'PEM', $key . $/ . $data ]; | 
| 89 |  |  |  |  |  |  | } elsif ( $line =~ m{^(-----BEGIN PUBLIC KEY-----)} ) { | 
| 90 | 2 |  |  |  |  | 7 | my $key = $1; | 
| 91 | 2 |  |  |  |  | 5 | my ( $ok, $data ) = _until_end( $fh, '-----END PUBLIC KEY-----' ); | 
| 92 | 2 | 50 |  |  |  | 7 | croak "could not parse PKCS8 pubkey: $data" unless defined $ok; | 
| 93 | 2 |  |  |  |  | 9 | push @keys, [ 'PKCS8', $key . $/ . $data ]; | 
| 94 |  |  |  |  |  |  |  | 
| 95 |  |  |  |  |  |  | } elsif ( $line =~ m{^(---- BEGIN SSH2 PUBLIC KEY ----)} ) { | 
| 96 | 8 |  |  |  |  | 19 | my $key = $1; | 
| 97 | 8 |  |  |  |  | 21 | my ( $ok, $data ) = _until_end( $fh, '---- END SSH2 PUBLIC KEY ----' ); | 
| 98 | 7 | 100 |  |  |  | 60 | croak "could not parse RFC4716 pubkey: $data" unless defined $ok; | 
| 99 | 5 |  |  |  |  | 20 | push @keys, [ 'RFC4716', $key . $/ . $data ]; | 
| 100 |  |  |  |  |  |  | } elsif ( | 
| 101 |  |  |  |  |  |  | # long enough for a RSA 4096-bit key, a bit too genereous | 
| 102 |  |  |  |  |  |  | # for ed25519 so probably should instead be done for each | 
| 103 |  |  |  |  |  |  | # key type | 
| 104 |  |  |  |  |  |  | $line =~ m{ | 
| 105 |  |  |  |  |  |  | (?(?ecdsa)-sha2-nistp256|ssh-(?ed25519|rsa)) [\t ]+? | 
| 106 |  |  |  |  |  |  | (?[A-Za-z0-9+/=]{64,717}) (?:[\t ]|$) }x | 
| 107 |  |  |  |  |  |  | ) { | 
| 108 | 3 |  |  | 3 |  | 1208 | push @keys, [ $+{type}, $+{prefix} . ' ' . $+{key} ]; | 
|  | 3 |  |  |  |  | 1017 |  | 
|  | 3 |  |  |  |  | 1259 |  | 
|  | 10 |  |  |  |  | 110 |  | 
| 109 |  |  |  |  |  |  | } | 
| 110 | 21 | 100 |  |  |  | 184 | croak "too many keys" if @keys > $max_keys; | 
| 111 |  |  |  |  |  |  | } | 
| 112 | 10 |  |  |  |  | 197 | return \@keys; | 
| 113 |  |  |  |  |  |  | } | 
| 114 |  |  |  |  |  |  |  | 
| 115 |  |  |  |  |  |  | # this (probably incorrectly) enforces RFC 4716 parsing on all of the | 
| 116 |  |  |  |  |  |  | # multiline formats so may not be correct for the other two formats, | 
| 117 |  |  |  |  |  |  | # though attempts are made at supporting them | 
| 118 |  |  |  |  |  |  | sub _until_end { | 
| 119 | 13 |  |  | 13 |  | 36 | my ( $fh, $fin ) = @_; | 
| 120 | 13 |  |  |  |  | 16 | my $ok; | 
| 121 | 13 |  |  |  |  | 25 | my $ret = ''; | 
| 122 | 13 |  |  |  |  | 43 | while ( my $line = readline $fh ) { | 
| 123 | 86 | 100 |  |  |  | 209 | die "too many input lines" if $. > $max_lines; | 
| 124 | 85 | 100 |  |  |  | 372 | if ( $line =~ m/^($fin)/ ) { | 
| 125 | 10 |  |  |  |  | 22 | $ret .= $1; | 
| 126 | 10 |  |  |  |  | 15 | $ok = 1; | 
| 127 | 10 |  |  |  |  | 18 | last; | 
| 128 |  |  |  |  |  |  | } | 
| 129 |  |  |  |  |  |  |  | 
| 130 |  |  |  |  |  |  | # RFC 4716 "implementations SHOULD be prepared to read files | 
| 131 |  |  |  |  |  |  | # using any of the common line termination sequence[s]" | 
| 132 | 75 |  |  |  |  | 259 | $line =~ s/(\012|\015|\015\012)$//; | 
| 133 |  |  |  |  |  |  |  | 
| 134 |  |  |  |  |  |  | # RFC 4716 "line[s] ... MUST NOT be longer than 72 8-bit bytes | 
| 135 |  |  |  |  |  |  | # excluding line termination characters" (TODO bytes vs. characters) | 
| 136 | 75 | 50 |  |  |  | 143 | return undef, "line $. too long" if length $line > 72; | 
| 137 |  |  |  |  |  |  |  | 
| 138 |  |  |  |  |  |  | # RFC 4716 ignore "key file header" fields as this code pretends | 
| 139 |  |  |  |  |  |  | # that it cannot recognize any | 
| 140 | 75 | 100 |  |  |  | 145 | if ( $line =~ m/:/ ) { | 
| 141 | 7 | 100 |  |  |  | 19 | if ( $line =~ m/\\$/ ) {    # backslash continues a line | 
| 142 | 4 |  |  |  |  | 5 | do { | 
| 143 | 186 |  |  |  |  | 303 | $line = readline $fh; | 
| 144 | 186 | 100 |  |  |  | 262 | return undef, "continued to EOF" if eof $fh; | 
| 145 | 185 |  |  |  |  | 410 | $line =~ s/(\012|\015|\015\012)$//; | 
| 146 | 185 | 50 |  |  |  | 474 | return undef, "line $. too long" if length $line > 72; | 
| 147 |  |  |  |  |  |  | } until $line !~ m/\\$/; | 
| 148 |  |  |  |  |  |  | } | 
| 149 | 6 |  |  |  |  | 20 | next; | 
| 150 |  |  |  |  |  |  | } | 
| 151 |  |  |  |  |  |  |  | 
| 152 |  |  |  |  |  |  | # RFC 4253 section 6.6 indicates there can be a "signature | 
| 153 |  |  |  |  |  |  | # format identifier"; those are KLUGE not supported by this | 
| 154 |  |  |  |  |  |  | # module as I don't know what that specific encoding looks like. | 
| 155 |  |  |  |  |  |  | # go with a sloppy Base64ish match, meanwhile, as that is what | 
| 156 |  |  |  |  |  |  | # OpenSSH generates as output | 
| 157 | 68 | 50 |  |  |  | 177 | if ( $line =~ m{^([A-Za-z0-9+/=]{1,72})$} ) { | 
| 158 | 68 |  |  |  |  | 140 | $ret .= $1 . $/; | 
| 159 | 68 |  |  |  |  | 185 | next; | 
| 160 |  |  |  |  |  |  | } | 
| 161 |  |  |  |  |  |  |  | 
| 162 |  |  |  |  |  |  | # support RFC 822 by way of RFC 1421 PEM header extensions that | 
| 163 |  |  |  |  |  |  | # begin with leading whitespace (sloppy, should only happen for | 
| 164 |  |  |  |  |  |  | # header lines) | 
| 165 | 0 | 0 |  |  |  | 0 | next if $line =~ m{^[ \t]}; | 
| 166 |  |  |  |  |  |  |  | 
| 167 |  |  |  |  |  |  | # support RFC 1421 PEM blank line (poorly, as all blank lines | 
| 168 |  |  |  |  |  |  | # are ignored) | 
| 169 | 0 | 0 |  |  |  | 0 | next if $line =~ m{^$}; | 
| 170 |  |  |  |  |  |  |  | 
| 171 | 0 |  |  |  |  | 0 | return undef, "fell off end of parser at line $."; | 
| 172 |  |  |  |  |  |  | } | 
| 173 | 11 |  |  |  |  | 36 | return $ok, $ret; | 
| 174 |  |  |  |  |  |  | } | 
| 175 |  |  |  |  |  |  |  | 
| 176 |  |  |  |  |  |  | 1; | 
| 177 |  |  |  |  |  |  | __END__ |