File Coverage

blib/lib/Data/SSHPubkey.pm
Criterion Covered Total %
statement 84 89 94.3
branch 36 52 69.2
condition n/a
subroutine 10 10 100.0
pod 2 2 100.0
total 132 153 86.2


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__