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   69232 use 5.010;
  3         23  
10 3     3   29 use strict;
  3         6  
  3         87  
11 3     3   16 use warnings;
  3         5  
  3         117  
12              
13 3     3   18 use Carp qw(croak);
  3         10  
  3         212  
14 3     3   2316 use File::Temp ();
  3         69566  
  3         2397  
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.06';
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 3529 my ($list) = @_;
47 1         2 my @pubkeys;
48 1         4 for my $ref (@$list) {
49 3 50       914 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         44 my $tmp = File::Temp->new;
54 3         2040 print $tmp $ref->[1];
55 3         17 my $tfile = $tmp->filename;
56 3 50       7837 open my $fh, '-|', qw(ssh-keygen -i -m), $ref->[0], '-f', $tfile
57             or die "could not exec ssh-keygen: $!";
58 3         26 binmode $fh;
59 3         49 push @pubkeys, do { local $/; readline $fh };
  3         83  
  3         7479  
60 3 50       306 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         441 chomp @pubkeys;
68 1         21 return \@pubkeys;
69             }
70              
71             sub pubkeys {
72 15     15 1 18323 my ($input) = @_;
73 15 100       75 croak "input must be string, GLOB, or scalar ref" if !defined $input;
74 14         19 my $fh;
75 14 100       53 if ( ref $input eq 'GLOB' ) {
76 1         12 $fh = $input;
77             } else {
78 13 50   2   462 open $fh, '<', $input or croak "could not open $input: $!";
  2         14  
  2         3  
  2         13  
79 13         1575 binmode $fh;
80             }
81 14         30 my @keys;
82 14         266 while ( my $line = readline $fh ) {
83 24 50       89 croak "too many input lines" if $. > $max_lines;
84 24 100       178 if ( $line =~ m{^(-----BEGIN RSA PUBLIC KEY-----)} ) {
    100          
    100          
    100          
85 3         46 my $key = $1;
86 3         25 my ( $ok, $data ) = _until_end( $fh, '-----END RSA PUBLIC KEY-----' );
87 3 50       9 croak "could not parse PEM pubkey: $data" unless defined $ok;
88 3         27 push @keys, [ 'PEM', $key . $/ . $data ];
89             } elsif ( $line =~ m{^(-----BEGIN PUBLIC KEY-----)} ) {
90 2         6 my $key = $1;
91 2         7 my ( $ok, $data ) = _until_end( $fh, '-----END PUBLIC KEY-----' );
92 2 50       6 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         22 my $key = $1;
97 8         26 my ( $ok, $data ) = _until_end( $fh, '---- END SSH2 PUBLIC KEY ----' );
98 7 100       49 croak "could not parse RFC4716 pubkey: $data" unless defined $ok;
99 5         24 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   1400 push @keys, [ $+{type}, $+{prefix} . ' ' . $+{key} ];
  3         1159  
  3         1473  
  10         113  
109             }
110 21 100       202 croak "too many keys" if @keys > $max_keys;
111             }
112 10         160 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   32 my ( $fh, $fin ) = @_;
120 13         19 my $ok;
121 13         27 my $ret = '';
122 13         51 while ( my $line = readline $fh ) {
123 86 100       211 die "too many input lines" if $. > $max_lines;
124 85 100       372 if ( $line =~ m/^($fin)/ ) {
125 10         33 $ret .= $1;
126 10         20 $ok = 1;
127 10         19 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         302 $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       171 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       143 if ( $line =~ m/:/ ) {
141 7 100       25 if ( $line =~ m/\\$/ ) { # backslash continues a line
142 4         5 do {
143 186         332 $line = readline $fh;
144 186 100       301 return undef, "continued to EOF" if eof $fh;
145 185         522 $line =~ s/(\012|\015|\015\012)$//;
146 185 50       590 return undef, "line $. too long" if length $line > 72;
147             } until $line !~ m/\\$/;
148             }
149 6         24 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       192 if ( $line =~ m{^([A-Za-z0-9+/=]{1,72})$} ) {
158 68         220 $ret .= $1 . $/;
159 68         193 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         38 return $ok, $ret;
174             }
175              
176             1;
177             __END__