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   67651 use 5.010;
  3         22  
10 3     3   19 use strict;
  3         4  
  3         84  
11 3     3   18 use warnings;
  3         6  
  3         106  
12              
13 3     3   17 use Carp qw(croak);
  3         5  
  3         215  
14 3     3   2180 use File::Temp ();
  3         66568  
  3         2372  
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 = '1.00';
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 3664 my ($list) = @_;
47 1         2 my @pubkeys;
48 1         5 for my $ref (@$list) {
49 3 50       941 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         58 my $tmp = File::Temp->new;
54 3         2231 print $tmp $ref->[1];
55 3         15 my $tfile = $tmp->filename;
56 3 50       7202 open my $fh, '-|', qw(ssh-keygen -i -m), $ref->[0], '-f', $tfile
57             or die "could not exec ssh-keygen: $!";
58 3         42 binmode $fh;
59 3         44 push @pubkeys, do { local $/; readline $fh };
  3         91  
  3         7276  
60 3 50       326 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         472 chomp @pubkeys;
68 1         21 return @pubkeys;
69             }
70              
71             sub pubkeys {
72 15     15 1 18541 my ($input) = @_;
73 15 100       96 croak "input must be string, GLOB, or scalar ref" if !defined $input;
74 14         22 my $fh;
75 14 100       52 if (ref $input eq 'GLOB') {
76 1         11 $fh = $input;
77             } else {
78 13 50   2   449 open $fh, '<', $input or croak "could not open $input: $!";
  2         17  
  2         23  
  2         16  
79 13         1796 binmode $fh;
80             }
81 14         32 my @keys;
82 14         232 while (my $line = readline $fh) {
83 24 50       91 croak "too many input lines" if $. > $max_lines;
84 24 100       184 if ($line =~ m{^(-----BEGIN RSA PUBLIC KEY-----)}) {
    100          
    100          
    100          
85 3         40 my $key = $1;
86 3         21 my ($ok, $data) = _until_end($fh, '-----END RSA PUBLIC KEY-----');
87 3 50       10 croak "could not parse PEM pubkey: $data" unless defined $ok;
88 3         55 push @keys, [ 'PEM', $key . $/ . $data ];
89             } elsif ($line =~ m{^(-----BEGIN PUBLIC KEY-----)}) {
90 2         8 my $key = $1;
91 2         8 my ($ok, $data) = _until_end($fh, '-----END PUBLIC KEY-----');
92 2 50       8 croak "could not parse PKCS8 pubkey: $data" unless defined $ok;
93 2         10 push @keys, [ 'PKCS8', $key . $/ . $data ];
94              
95             } elsif ($line =~ m{^(---- BEGIN SSH2 PUBLIC KEY ----)}) {
96 8         25 my $key = $1;
97 8         21 my ($ok, $data) = _until_end($fh, '---- END SSH2 PUBLIC KEY ----');
98 7 100       51 croak "could not parse RFC4716 pubkey: $data" unless defined $ok;
99 5         22 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   1371 push @keys, [ $+{type}, $+{prefix} . ' ' . $+{key} ];
  3         1215  
  3         1430  
  10         125  
109             }
110 21 100       249 croak "too many keys" if @keys > $max_keys;
111             }
112 10         158 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   37 my ($fh, $fin) = @_;
120 13         22 my $ok;
121 13         27 my $ret = '';
122 13         47 while (my $line = readline $fh) {
123 86 100       192 die "too many input lines" if $. > $max_lines;
124 85 100       417 if ($line =~ m/^($fin)/) {
125 10         24 $ret .= $1;
126 10         14 $ok = 1;
127 10         23 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         283 $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       163 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       147 if ($line =~ m/:/) {
141 7 100       22 if ($line =~ m/\\$/) { # backslash continues a line
142 4         9 do {
143 186         337 $line = readline $fh;
144 186 100       311 return undef, "continued to EOF" if eof $fh;
145 185         519 $line =~ s/(\012|\015|\015\012)$//;
146 185 50       559 return undef, "line $. too long" if length $line > 72;
147             } until $line !~ m/\\$/;
148             }
149 6         25 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       199 if ($line =~ m{^([A-Za-z0-9+/=]{1,72})$}) {
158 68         152 $ret .= $1 . $/;
159 68         191 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         44 return $ok, $ret;
174             }
175              
176             1;
177             __END__