README: add some headings and .md alias
[ezcert.git] / ConvertPubKey
1 #!/usr/bin/perl
2
3 # ConvertPubKey - Convert public keys to/from OpenSSH/X509 format
4 # Copyright (c) 2011,2012,2013,2014 Kyle J. McKay.  All rights reserved.
5
6 # *** See detailed help starting around line 80 ***
7
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU Affero General Public License for more details.
17 #
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21 exit(&main());
22
23 use strict;
24 use warnings;
25 use bytes;
26
27 use MIME::Base64;
28 use IPC::Open2;
29 use Digest::MD5 qw(md5 md5_hex md5_base64);
30 use Getopt::Long qw(:config gnu_getopt);
31
32 our $VERSION;
33 my $VERSIONMSG;
34 my $HELP;
35 my $USAGE;
36
37 BEGIN {
38   *VERSION = \'1.0.4';
39   $VERSIONMSG = "ConvertPubKey version $VERSION\n" .
40     "Copyright (c) 2011-2014 Kyle J. McKay.  All rights reserved.\n" .
41     "License AGPLv3+: GNU Affero GPL version 3 or later.\n" .
42     "http://gnu.org/licenses/agpl.html\n" .
43     "This is free software: you are free to change and redistribute it.\n" .
44     "There is NO WARRANTY, to the extent permitted by law.\n";
45 }
46
47 BEGIN {
48   eval {
49     require Digest::SHA1;
50     Digest::SHA1->import(
51       qw(
52         sha1 sha1_hex sha1_base64
53       )
54     ); 1} ||
55   eval {
56     require Digest::SHA;
57     Digest::SHA->import(
58       qw(
59         sha1 sha1_hex sha1_base64
60       )
61     ); 1} ||
62   eval {
63     require Digest::SHA::PurePerl;
64     Digest::SHA::PurePerl->import(
65       qw(
66         sha1 sha1_hex sha1_base64
67       )
68     ); 1} ||
69   die "One of Digest::SHA1 or Digest::SHA or Digest::SHA::PurePerl "
70     . "must be available\n";
71 }
72
73 BEGIN {
74   $USAGE = <<'USAGE';
75 Usage: ConvertPubKey [-h] [--version] [--verbose] [--debug] [--pubx509] [-t]
76          [--format ssh|pem|der] [--in pub_key_file] [--out out_file.pub]
77          [--quiet] [--check] [optional comment here]
78 USAGE
79   $HELP = <<'HELP';
80 NAME
81        ConvertPubKey -- convert public key formats
82
83 SYNOPSIS
84        ConvertPubKey [-h] [--version] [--verbose] [--debug] [--pubx509] [-t]
85          [--format ssh|pem|der] [--in pub_key_file] [--out out_file.pub]
86          [--quiet] [--check] [optional comment here]
87
88 DESCRIPTION
89        ConvertPubKey converts public keys to/from OpenSSH/X509 format.
90
91        Input may be an X.509 public key in either PEM or DER format or an
92        OpenSSH public key.
93
94        Output defaults to the other kind of key so that if input is an OpenSSH
95        public key, output will be an X.509 PEM public key and if input is an
96        X.509 PEM or DER public key, output will be an OpenSSH public key.
97
98        When outputing an OpenSSH public key, the comment, if any will be
99        placed at the end of the line separated from the Base64 of the OpenSSH
100        public key by a space (this is the normal location for an OpenSSH public
101        key comment).  Web sites that accept OpenSSH public keys often require
102        the comment to be of the form user@host or they will reject the public
103        key.
104
105        No validation is performed on the "optional comment here" value.
106
107 OPTIONS
108        -h/--help
109               Show this help
110
111        -V/--version
112               Show the ConvertPubKey version
113
114        -v/--verbose
115               Produce extra informational messages to standard error.
116
117        --debug
118               Show debugging information.  Automatically enables --verbose.
119               Suppresses --quiet.
120
121        --quiet
122               Suppress all messages except errors.  Ignored if --debug or
123               --verbose given.
124
125        --check
126               Perform all normal validation checks but do not actually output a
127               public key.  Automatically enables --verbose.
128
129        --pubx509/--pubX509
130               Force the public key read from standard input to be interpreted
131               as an X.509 format public key.  Normally this should be
132               automatically detected and this option should not be needed.
133
134        -t
135               Allow reading the public key from standard input when standard
136               input is a tty.  In most cases attempting to read the public key
137               from standard input that is a tty indicates that the public key
138               was accidentally omitted.  If that is not the case, the -t option
139               must be given to allow reading the public key from standard input
140               when standard input is a tty.  This option is always implied if
141               the --in option is used with a value other than "-".
142
143        --format ssh|pem|der
144               Normally the output format is automatically chosen based on the
145               input format.  If the input format is an X.509 DER or PEM format
146               public key, the output format will be an OpenSSH public key.  If
147               the input format is an OpenSSH public key, the output format will
148               be an X.509 PEM format public key.  This option can be used to
149               override the default output format.  Using "ssh" requests the
150               output be an OpenSSH format public key.  Giving "pem" requests the
151               output be an X.509 PEM format public key and "der" requests output
152               be an X.509 DER format public key.
153
154        --in pub_key_file
155               The public key to convert.  May be an OpenSSH protocol 2 format
156               public key or an X.509 format public key (in either PEM or DER
157               format).  See also the --pubx509 option.  If pub_key_file is "-"
158               or this option is omitted then standard input is read.
159
160        --out out_file.pub
161               The converted public key will be written to out_file.pub.  If
162               this option is omitted or out_file.pub is "-" then the converted
163               public key is written to standard output.
164
165 NOTES
166        Only minimal checking is done on the input public key.
167
168        OpenSSH ssh-rsa and ssh-dss key types (and their corresponding X.509
169        equivalents) are supported, other types will result in errors.
170
171        Neither OpenSSH nor OpenSSL are needed to perform the key conversion.
172
173 TIPS
174        Create a new 2048-bit RSA private key in new_rsa_2048 with OpenSSL:
175
176            openssl genrsa -f4 -out new_rsa_2048 2048
177
178        Store the OpenSSH format public key for new_rsa_2048 in the file
179        new_rsa_2048.pub with comment "test@example.com":
180
181            openssl rsa -in new_rsa_2048 -pubout | \ 
182              ConvertPubKey --out new_rsa_2048.pub test@example.com
183
184 HELP
185 }
186
187 sub formatbold($;$)
188 {
189   my $str = shift;
190   my $fancy = shift || 0;
191   if ($fancy) {
192     $str = join('',map($_."\b".$_, split(//,$str)));
193   }
194   return $str;
195 }
196
197 sub formatul($;$)
198 {
199   my $str = shift;
200   my $fancy = shift || 0;
201   if ($fancy) {
202     $str = join('',map("_\b".$_, split(//,$str)));
203   }
204   return $str;
205 }
206
207 sub formatman($;$)
208 {
209   my $man = shift;
210   my $fancy = shift || 0;
211   my @inlines = split(/\n/, $man, -1);
212   my @outlines = ();
213   foreach my $line (@inlines) {
214     if ($line =~ /^[A-Z]+$/) {
215       $line = formatbold($line, $fancy);
216     }
217     else {
218       $line =~ s/'''(.+?)'''/formatbold($1,$fancy)/gse;
219       $line =~ s/''(.+?)''/formatul($1,$fancy)/gse;
220     }
221     push (@outlines, $line);
222   }
223   my $result = join("\n", @outlines);
224   $result =~ s/\\\n//gso;
225   return $result;
226 }
227
228 sub DERLength($)
229 {
230   # return a DER encoded length
231   my $len = shift;
232   return pack('C',$len) if $len <= 127;
233   return pack('C2',0x81, $len) if $len <= 255;
234   return pack('Cn',0x82, $len) if $len <= 65535;
235   return pack('CCn',0x83, ($len >> 16), $len & 0xFFFF) if $len <= 16777215;
236   # Silently returns invalid result if $len > 2^32-1
237   return pack('CN',0x84, $len);
238 }
239
240 sub SingleOID($)
241 {
242   # return a single DER encoded OID component
243   no warnings;
244   my $num = shift;
245   $num += 0;
246   my $result = pack('C', $num & 0x7F);
247   $num >>= 7;
248   while ($num) {
249     $result = pack('C', 0x80 | ($num & 0x7F)) . $result;
250     $num >>= 7;
251   }
252   return $result;
253 }
254
255 sub DEROID($)
256 {
257   # return a DER encoded OID complete with leading 0x06 and DER length
258   # Input is a string of decimal numbers separated by '.' with at least
259   # two numbers required.
260   no warnings;
261   my @ids = split(/[.]/,$_[0]);
262   push(@ids, 0) while @ids < 2; # return something that's kind of valid
263   unshift(@ids, shift(@ids) * 40 + shift(@ids)); # combine first two
264   my $ans = '';
265   foreach my $num (@ids) {
266     $ans .= SingleOID($num);
267   }
268   return pack('C',0x6).DERLength(length($ans)).$ans;
269 }
270
271 sub ReadDERLength($)
272 {
273   # Input is a DER encoded length with possibly extra trailing bytes
274   # Output is an array of length and bytes-used-for-encoded-length
275   my $der = shift;
276   return undef unless length($der);
277   my $byte = unpack('C',substr($der,0,1));
278   return ($byte, 1) if $byte <= 127;
279   return undef if $byte == 128 || $byte > 128+8; # Fail if greater than 2^64
280   my $cnt = $byte & 0x7F;
281   return undef unless length($der) >= $cnt+1; # Fail if not enough bytes
282   my $val = 0;
283   for (my $i = 0; $i < $cnt; ++$i) {
284     $val <<= 8;
285     $val |= unpack('C',substr($der,$i+1,1));
286   }
287   return ($val, $cnt+1);
288 }
289
290 sub CountKeyBits($)
291 {
292   my $data = shift;
293   return undef if length($data) < 1;
294   my $bits = 8 * length($data);
295   # but leading zero bits must be subtracted
296   my $byte = unpack('C',substr($data,0,1));
297   if (!$byte) {
298     $bits -= 8;
299   } else {
300     return undef if $byte & 0x80; # negative is not valid
301     while (!($byte & 0x80)) {
302       --$bits;
303       $byte <<= 1;
304     }
305   }
306   return $bits;
307 }
308
309 sub PackUIntBytes($)
310 {
311   my $intbytes = shift || '';
312   return pack('C2',0x02,0x00) unless length($intbytes);
313   my $byte = unpack('C',substr($intbytes,0,1));
314   $intbytes = pack('C',0x00).$intbytes if $byte & 0x80; # would've been negative
315   return pack('C',0x02).DERLength(length($intbytes)).$intbytes;
316 }
317
318 sub GetOpenSSHKeyInfo($)
319 {
320   # Input is an OpenSSH public key in .pub format
321   # Output is an array of:
322   #   key type (either 'rsa' or 'dsa')
323   #   how many bits in the modulus
324   #   the public exponent (if RSA) or how many bits in prime divisor (if DSA)
325   #   the key id
326   #   the OpenSSH md5 fingerprint
327   #   the OpenSSH sha1 fingerprint
328   #   the OpenSSH comment (may be '')
329   #   the OpenSSH public key in OpenSSL PUBLIC KEY DER format
330   # or undef if the key is unparseable
331   # or just the key type if it's not ssh-rsa
332   #
333   # Expected format is:
334   #   ssh-rsa BASE64PUBLICKEYDATA optional comment here
335   # or
336   #   ssh-dss BASE64PUBLICKEYDATA optional comment here
337   # where the BASE64PUBLICKEYDATA when decoded produces:
338   #   RSA:
339   #     4 Byte Big-Endian length of Key type (must be 7 for RSA)
340   #     Key type WITHOUT terminating NUL (must be ssh-rsa for RSA)
341   #     4 Byte Big-Endian length of public exponent e
342   #     Public exponent e integer bytes
343   #     4 Byte Big-Endian length of modulus n
344   #     Modulus n integer bytes
345   #   DSS:
346   #     4 Byte Big-Endian length of Key type (must be 7 for DSS)
347   #     Key type WITHOUT terminating NUL (must be ssh-dss for DSS)
348   #     4 Byte Big-Endian length of prime modulus p
349   #     Prime modulus p integer bytes
350   #     4 Byte Big-Endian length of prime divisor q
351   #     Prime divisor q integer bytes
352   #     4 Byte Big-Endian length of parameter g
353   #     Parameter g integer bytes
354   #     4 Byte Big-Endian length of public key y
355   #     Public key y integer bytes
356   # no extra trailing bytes are permitted
357   my $input = shift;
358   $input =~ s/((?:\r\n|\n|\r).*)$//os;
359   my @fields = split(' ', $input, 3);
360   return undef unless @fields >= 2;
361   my $data = decode_base64($fields[1]);
362   my $origData = $data;
363   my @parts = ();
364   while (length($data) >= 4) {
365     my $len = unpack('N',substr($data,0,4));
366     my $value = '';
367     if ($len > 0) {
368       return undef if $len + 4 > length($data);
369       $value = substr($data,4,$len);
370     }
371     push(@parts, $value);
372     substr($data, 0, 4+$len) = '';
373   }
374   return undef unless length($data) == 0;
375   return $parts[0] if @parts >= 1 && $parts[0] &&
376                               $parts[0] ne 'ssh-rsa' && $parts[0] ne 'ssh-dss';
377   if ($parts[0] eq 'ssh-rsa') {
378     return undef unless @parts == 3;
379     my $rsaEncryption = DEROID('1.2.840.113549.1.1.1'); # :rsaEncryption
380     $rsaEncryption = pack('C',0x30).DERLength(length($rsaEncryption)+2)
381       .$rsaEncryption.pack('C2',0x05,0x00);
382     my $pubrsa = PackUIntBytes($parts[2]); # modulus
383     $pubrsa .= PackUIntBytes($parts[1]); # exponent
384     $pubrsa = pack('C',0x30).DERLength(length($pubrsa)).$pubrsa;
385     my $id = sha1($pubrsa); # The id is the sha1 hash of the BIT STRING part
386     $pubrsa = pack('C',0x3).DERLength(length($pubrsa)+1).pack('C',0x0).$pubrsa;
387     $pubrsa = $rsaEncryption.$pubrsa;
388     $pubrsa = pack('C',0x30).DERLength(length($pubrsa)).$pubrsa;
389
390     my $bits = CountKeyBits($parts[2]);
391     return undef unless $bits;
392
393     my $rawexp = $parts[1];
394     my $exp;
395     if (length($rawexp) > 8) {
396       # Fudge the result because it's bigger than a 64-bit number
397       my $lastbyte = unpack('C',substr($rawexp,-1,1));
398       $exp = $lastbyte & 0x01 ? 65537 : 65536;
399     }
400     else {
401       $exp = 0;
402       while (length($rawexp)) {
403         $exp <<= 8;
404         $exp |= unpack('C',substr($rawexp,0,1));
405         substr($rawexp,0,1) = '';
406       }
407     }
408     my $cmnt = $fields[2]||'';
409     return
410       ('rsa',$bits,$exp,$id,md5($origData),sha1($origData),$cmnt,$pubrsa);
411   } else {
412     return undef unless @parts == 5;
413     my $dsaEncryption = DEROID('1.2.840.10040.4.1'); # :dsaEncryption
414     my $algparms = PackUIntBytes($parts[1]). # prime modulus p
415                    PackUIntBytes($parts[2]). # prime divisor q
416                    PackUIntBytes($parts[3]); # parameter g
417     $algparms = pack('C',0x30).DERLength(length($algparms)).$algparms;
418     $dsaEncryption = pack('C',0x30).
419                      DERLength(length($dsaEncryption) + length($algparms)).
420                      $dsaEncryption.$algparms;
421     my $pubdsa = PackUIntBytes($parts[4]); # public key y
422     my $id = sha1($pubdsa); # The id is the sha1 hash of the BIT STRING part
423     $pubdsa = pack('C',0x3).DERLength(length($pubdsa)+1).pack('C',0x0).$pubdsa;
424     $pubdsa = $dsaEncryption.$pubdsa;
425     $pubdsa = pack('C',0x30).DERLength(length($pubdsa)).$pubdsa;
426
427     my $bits = CountKeyBits($parts[1]);
428     return undef unless $bits;
429     my $divsiz = CountKeyBits($parts[2]);
430     return undef unless $divsiz;
431
432     my $cmnt = $fields[2]||'';
433     return
434       ('dsa',$bits,$divsiz,$id,md5($origData),sha1($origData),$cmnt,$pubdsa);
435   }
436 }
437
438 sub GetKeyInfo($)
439 {
440   # Input is a X.509 PUBLIC KEY (RSA or DSS) in DER format
441   # Output is an array of:
442   #   key type (either 'rsa' or 'dsa')
443   #   how many bits in the modulus
444   #   the public exponent (if RSA) or how many bits in prime divisor (if DSA)
445   #   the key id
446   #   the OpenSSH md5 fingerprint
447   #   the OpenSSH sha1 fingerprint
448   #   the OpenSSH public key in .pub format
449   # or undef if the key is unparseable
450   #
451   # Expected format for an RSA public key is:
452   #   SEQUENCE {
453   #     SEQUENCE {
454   #       OBJECT IDENTIFIER :rsaEncryption = 1.2.840.113549.1.1.1
455   #       NULL
456   #     }
457   #     BIT STRING (primitive) {
458   #       0 unused bits
459   #       SEQUENCE { # this part is the contents of an "RSA PUBLIC KEY" file
460   #         INTEGER modulus n
461   #         INTEGER publicExponent e
462   #       }
463   #     }
464   #   }
465   #
466   # Expected format for a DSA public key is:
467   #   SEQUENCE {
468   #     SEQUENCE {
469   #       OBJECT IDENTIFIER :dsaEncryption = 1.2.840.10040.4.1
470   #       SEQUENCE {
471   #         INTEGER prime modulus p
472   #         INTEGER prime divisor q
473   #         INTEGER parameter g
474   #       }
475   #     }
476   #     BIT STRING (primitive) {
477   #       0 unused bits
478   #       INTEGER public key y
479   #     }
480   #   }
481
482   no warnings;
483   my $der = shift;
484   my $rawmod;
485   my $rawexp;
486
487   return undef if unpack('C',substr($der,0,1)) != 0x30;
488   my ($len, $lenbytes) = ReadDERLength(substr($der,1));
489   return undef unless length($der) == 1 + $lenbytes + $len;
490   substr($der, 0, 1 + $lenbytes) = '';
491
492   return undef if unpack('C',substr($der,0,1)) != 0x30;
493   ($len, $lenbytes) = ReadDERLength(substr($der,1));
494   return undef unless length($der) >= 1 + $lenbytes + $len;
495   my $ident = substr($der, 1 + $lenbytes, $len);
496   substr($der, 0, 1 + $lenbytes + $len) = '';
497
498   return undef unless unpack('C',substr($ident,0,1)) == 0x06; # OID
499   ($len, $lenbytes) = ReadDERLength(substr($ident,1));
500   return undef unless length($ident) > 1 + $lenbytes + $len;
501   my $keytypeoid = substr($ident, 0, 1 + $lenbytes + $len);
502   substr($ident, 0, 1 + $lenbytes + $len) = '';
503
504   # $keytypeoid may be either rsaEncryption or dsaEncryption
505   my $rsaEncryption = DEROID('1.2.840.113549.1.1.1'); # :rsaEncryption
506   my $dsaEncryption = DEROID('1.2.840.10040.4.1'); # :dsaEncryption
507
508   return undef
509     unless $keytypeoid eq $rsaEncryption || $keytypeoid eq $dsaEncryption;
510
511   return undef if unpack('C',substr($der,0,1)) != 0x03;
512   ($len, $lenbytes) = ReadDERLength(substr($der,1));
513   return undef unless length($der) == 1 + $lenbytes + $len && $len >= 1;
514   return undef unless unpack('C',substr($der, 1 + $lenbytes, 1)) == 0x00;
515   substr($der, 0, 1 + $lenbytes + 1) = '';
516   my $id = sha1($der); # The id is the sha1 hash of the BIT STRING contents
517
518   if ($keytypeoid eq $rsaEncryption) {
519     return undef unless $ident eq pack('C2', 0x05, 0x00);
520
521     return undef if unpack('C',substr($der,0,1)) != 0x30;
522     ($len, $lenbytes) = ReadDERLength(substr($der,1));
523     return undef unless length($der) == 1 + $lenbytes + $len;
524     substr($der, 0, 1 + $lenbytes) = '';
525
526     return undef if unpack('C',substr($der,0,1)) != 0x02;
527     ($len, $lenbytes) = ReadDERLength(substr($der,1));
528     substr($der, 0, 1 + $lenbytes) = '';
529     my $derexp = substr($der, $len);
530     substr($der, $len) = '';
531     return undef unless $len >= 1;
532     $rawmod = $der;
533     my $bits = CountKeyBits($der);
534     return undef unless $bits;
535
536     $der = $derexp;
537     return undef if unpack('C',substr($der,0,1)) != 0x02;
538     ($len, $lenbytes) = ReadDERLength(substr($der,1));
539     substr($der, 0, 1 + $lenbytes) = '';
540     return undef unless length($der) == $len && $len >= 1;
541     return undef if unpack('C',substr($der,0,1)) & 0x80; # negative pub exp bad
542     $rawexp = $der;
543     my $exp;
544     if ($len > 8) {
545       # Fudge the result because it's bigger than a 64-bit number
546       my $lastbyte = unpack('C',substr($der,-1,1));
547       $exp = $lastbyte & 0x01 ? 65537 : 65536;
548     }
549     else {
550       $exp = 0;
551       while (length($der)) {
552         $exp <<= 8;
553         $exp |= unpack('C',substr($der,0,1));
554         substr($der,0,1) = '';
555       }
556     }
557
558     my $sshbin = pack('N',7)."ssh-rsa".pack('N',length($rawexp)).$rawexp
559       .pack('N',length($rawmod)).$rawmod;
560     my $sshpub = "ssh-rsa " . encode_base64($sshbin,'');
561
562     return ('rsa',$bits,$exp,$id,md5($sshbin),sha1($sshbin),$sshpub);
563
564   } else {
565     return undef unless unpack('C',substr($ident,0,1)) == 0x30;
566     ($len, $lenbytes) = ReadDERLength(substr($ident,1));
567     return undef unless length($ident) == 1 + $lenbytes + $len;
568     substr($ident, 0, 1 + $lenbytes) = '';
569
570     return undef unless unpack('C',substr($ident,0,1)) == 0x02;
571     ($len, $lenbytes) = ReadDERLength(substr($ident,1));
572     return undef unless length($ident) > 1 + $lenbytes + $len;
573     substr($ident, 0, 1 + $lenbytes) = '';
574     my $dsa_p = substr($ident, 0, $len);
575     substr($ident, 0, $len) = '';
576
577     return undef unless unpack('C',substr($ident,0,1)) == 0x02;
578     ($len, $lenbytes) = ReadDERLength(substr($ident,1));
579     return undef unless length($ident) > 1 + $lenbytes + $len;
580     substr($ident, 0, 1 + $lenbytes) = '';
581     my $dsa_q = substr($ident, 0, $len);
582     substr($ident, 0, $len) = '';
583
584     return undef unless unpack('C',substr($ident,0,1)) == 0x02;
585     ($len, $lenbytes) = ReadDERLength(substr($ident,1));
586     return undef unless length($ident) == 1 + $lenbytes + $len;
587     substr($ident, 0, 1 + $lenbytes) = '';
588     my $dsa_g = substr($ident, 0, $len);
589
590     return undef unless unpack('C',substr($der,0,1)) == 0x02;
591     ($len, $lenbytes) = ReadDERLength(substr($der,1));
592     return undef unless length($der) == 1 + $lenbytes + $len;
593     my $dsa_y = substr($der, 1 + $lenbytes, $len);
594
595     my $bits = CountKeyBits($dsa_p);
596     return undef unless $bits;
597     my $divsiz = CountKeyBits($dsa_q);
598     return undef unless $divsiz;
599
600     my $sshbin = pack('N',7)."ssh-dss".pack('N',length($dsa_p)).$dsa_p
601       .pack('N',length($dsa_q)).$dsa_q.pack('N',length($dsa_g)).$dsa_g
602       .pack('N',length($dsa_y)).$dsa_y;
603     my $sshpub = "ssh-dss " . encode_base64($sshbin,'');
604
605     return ('dsa',$bits,$divsiz,$id,md5($sshbin),sha1($sshbin),$sshpub);
606   }
607 }
608
609 my %rsadsa_known_strengths;
610 BEGIN {
611   %rsadsa_known_strengths = (
612     1024 => 80,
613     2048 => 112,
614     3072 => 128,
615     7680 => 192,
616     15360 => 256,
617   );
618 }
619
620 sub compute_rsadsa_strength($)
621 {
622   my $rsadsabits = shift;
623   return 0 unless $rsadsabits && $rsadsabits > 0;
624   return $rsadsa_known_strengths{$rsadsabits} if $rsadsa_known_strengths{$rsadsabits};
625   my $guess;
626   if ($rsadsabits < 1024) {
627     $guess = 80 * sqrt($rsadsabits/1024);
628   } elsif ($rsadsabits > 15360) {
629     $guess = 256 * sqrt($rsadsabits/15360);
630   } else {
631     $guess = 34.141 + sqrt(34.141*34.141 - 4*0.344*(1554.7-$rsadsabits));
632     $guess = $guess / (2 * 0.344);
633   }
634   $guess = 79 if $rsadsabits < 1024 && $guess >= 80;
635   $guess = 80 if $rsadsabits > 1024 && $guess < 80;
636   $guess = 111 if $rsadsabits > 1024 && $rsadsabits < 2048 && $guess >= 112;
637   $guess = 112 if $rsadsabits > 2048 && $guess < 112;
638   $guess = 127 if $rsadsabits > 2048 && $rsadsabits < 3072 && $guess >= 128;
639   $guess = 128 if $rsadsabits > 3072 && $guess < 128;
640   $guess = 191 if $rsadsabits > 3072 && $rsadsabits < 7680 && $guess >= 192;
641   $guess = 192 if $rsadsabits > 7680 && $guess < 192;
642   $guess = 255 if $rsadsabits > 7680 && $rsadsabits < 15360 && $guess >= 256;
643   $guess = 256 if $rsadsabits > 15360 && $guess < 256;
644   return int($guess) . ' (approximately)';
645 }
646
647 sub BreakLine($$)
648 {
649   my ($line,$width) = @_;
650   my @ans = ();
651   return $line if $width < 1;
652   while (length($line) > $width) {
653     push(@ans, substr($line, 0, $width));
654     substr($line, 0, $width) = '';
655   }
656   push(@ans, $line) if length($line);
657   return @ans;
658 }
659
660 sub main
661 {
662   my $help = '';
663   my $verbose = '';
664   my $quiet = '';
665   my $termOK = '';
666   my $debug = 0;
667   my $pubx509 = '';
668   my $check = '';
669   my $format = undef;
670   my $infile = '-';
671   my $outfile = '-';
672
673   #tests;
674   eval {GetOptions(
675       "help|h" => sub{$help=1;die"!FINISH"},
676       "verbose|v" => \$verbose,
677       "version|V" => sub{print STDERR $VERSIONMSG;exit(0)},
678       "debug" => \$debug,
679       "quiet" => \$quiet,
680       "pubx509" => \$pubx509,
681       "pubX509" => \$pubx509,
682       "check" => \$check,
683       "t" => \$termOK,
684       "format=s" => \$format,
685       "in=s" => \$infile,
686       "out=s" => \$outfile
687     )} || $help
688       or die $USAGE;
689   if ($help) {
690     local *MAN;
691     my $pager = $ENV{'PAGER'} || 'less';
692     if (-t STDOUT && open(MAN, "|-", $pager)) {
693       print MAN formatman($HELP,1);
694       close(MAN);
695     }
696     else {
697       print formatman($HELP);
698     }
699     exit(0);
700   }
701   die "--in requires a filename\n" if !$infile;
702   die "--out requires a filename\n" if !$outfile;
703   if (defined($format)) {
704     $format = lc($format);
705     die "--format argument must be 'ssh', 'pem' or 'der'\n"
706       if $format ne 'ssh' && $format ne 'pem' && $format ne 'der';
707   }
708   $verbose = 1 if $debug || $check;
709   print STDERR $VERSIONMSG if $verbose;
710   my $keytype = 'OpenSSH';
711   $keytype = 'X.509' if $pubx509;
712   die "Standard input is a tty (which is an unlikely source of a "
713     . "public key)\n"
714     . "If that's what you truly meant, add the -t option to allow it.\n"
715     if $infile eq '-' && -t STDIN && !$termOK;
716   my $opensshdotpub;
717   my $infilename;
718   {
719     local $/ if $pubx509;
720     my $input;
721     if ($infile ne '-') {
722       $infilename = "\"$infile\"";
723       open($input, '<', $infile)
724         or die "Cannot open $infilename for input: $!\n";
725     } else {
726       $input = *STDIN;
727       $infilename = 'standard input';
728     }
729     !!($opensshdotpub = <$input>)
730       or die "Cannot read $keytype public key from $infilename\n";
731     if (!$pubx509) {
732       my $auto509 = 0;
733       if ($opensshdotpub =~ /^----[- ]BEGIN PUBLIC KEY[- ]----/) {
734         $auto509 = 1;
735       }
736       else {
737         my $input = $opensshdotpub;
738         $input =~ s/((?:\r\n|\n|\r).*)$//os;
739         my @fields = split(' ', $input, 3);
740         if (@fields < 2 ||
741             length($fields[1]) < 16 ||
742             $fields[1] !~ m|^[0-9A-Za-z+/=]+$|) {
743           $auto509 = 1;
744         }
745       }
746       if ($auto509) {
747         $pubx509 = 1;
748         $keytype = 'X.509';
749         print STDERR "auto detected --pubx509 option\n" if $debug;
750         local $/;
751         my $extra = <$input>;
752         $opensshdotpub .= $extra if $extra;
753       }
754     }
755     close($input) if $infile ne '-';
756   }
757
758   my ($kk, $sshkeybits, $sshkeyexp, $sshkeyid, $sfmd5, $sfsha1, $sshcmnt);
759   my ($opensshpub, $dotpub);
760   if ($pubx509) {
761     my $inform =
762       ($opensshdotpub =~ m|^[\t\n\r\x20-\x7E\x80-\xFF]*$|os) ? 'PEM' : 'DER';
763     print STDERR "pubx509 -inform $inform\n" if $debug;
764     my $bin = 1;
765     if ($inform eq 'PEM') {
766       $opensshpub = $opensshdotpub;
767       if ($opensshpub =~ /^----[- ]BEGIN/m) {
768         $opensshpub = "\n".$opensshpub."\n";
769         $opensshpub =~ s/\A.*?\n----[- ]BEGIN[^\n]*\n//s;
770         $opensshpub =~ s/\n----[- ]END[^\n]*\n.*\z//s;
771         $opensshpub =~ tr/[ \t\r\n]//d;
772         $bin = 0;
773       } elsif ($opensshpub =~ m,\A[\t\r\n A-Za-z0-9+/=]*\z,s) {
774         $opensshpub =~ tr/[ \t\r\n]//d;
775         $bin = 0;
776       }
777     } else {
778       $opensshpub = $opensshdotpub;
779     }
780     print STDERR "BASE64 X509: $opensshpub\n" if !$bin && $debug;
781     $opensshpub = decode_base64($opensshpub) if !$bin;
782     print STDERR "HEX X509: ", unpack('H*', $opensshpub), "\n" if $debug;
783     $sshcmnt = undef;
784     ($kk,$sshkeybits,$sshkeyexp,$sshkeyid,$sfmd5,$sfsha1,$dotpub) =
785       GetKeyInfo($opensshpub);
786     die "Unparseable X.509 public key format read from $infilename\n"
787       unless $sshkeybits;
788     ($kk,$sshkeybits,$sshkeyexp,$sshkeyid,$sfmd5,$sfsha1,$sshcmnt,$opensshpub) =
789       GetOpenSSHKeyInfo($dotpub);
790   } else {
791     ($kk,$sshkeybits,$sshkeyexp,$sshkeyid,$sfmd5,$sfsha1,$sshcmnt,$opensshpub) =
792       GetOpenSSHKeyInfo($opensshdotpub);
793     die "Unparseable OpenSSH public key read from $infilename\n"
794       unless $sshkeybits;
795     ($kk,$sshkeybits,$sshkeyexp,$sshkeyid,$sfmd5,$sfsha1,$dotpub) =
796       GetKeyInfo($opensshpub);
797   }
798   my $keykind = uc($kk);
799   if ($verbose) {
800     my $strength = compute_rsadsa_strength($sshkeybits);
801     printf STDERR "$keytype $keykind Public Key Info:\n".
802       "  bits=$sshkeybits %s=$sshkeyexp %s\n", $kk eq 'rsa'?"pubexp":"divsiz",
803       "secstrength=$strength";
804   }
805   print STDERR "  keyid=",
806     join(":", uc(unpack("H*",$sshkeyid))=~/../g), "\n" if $verbose;
807   print STDERR "  fingerprint(md5)=",
808     join(":", lc(unpack("H*",$sfmd5))=~/../g), "\n" if $verbose;
809   print STDERR "  fingerprint(sha1)=",
810     join(":", lc(unpack("H*",$sfsha1))=~/../g), "\n" if $verbose;
811   print STDERR "  comment=",$sshcmnt||'<none present>',"\n"
812     if $verbose && !$pubx509;
813   die "*** Error: $keytype key has less than 512 bits ($sshkeybits)\n"
814     . "***        You might as well just donate your system to hackers now.\n"
815     if $sshkeybits < 512;
816   die "*** Error: The $keytype key's public exponent is even ($sshkeyexp)!\n"
817     if $kk eq 'rsa' && $sshkeyexp && !($sshkeyexp & 0x01);
818   warn "*** Warning: The $keytype key has less than 2048 bits ($sshkeybits), "
819     . "continuing anyway\n" if !$quiet && $sshkeybits < 2048;
820   die "*** Error: The $keytype public key's exponent of $sshkeyexp is "
821     . "unacceptably weak!\n"
822     if $kk eq 'rsa' && $sshkeyexp && $sshkeyexp < 35; # OpenSSH used 35 until v5.4
823   warn "*** Warning: The $keytype public key's exponent ($sshkeyexp) is weak "
824     . "(< 65537), continuing anyway\n"
825     if !$quiet && $kk eq 'rsa' && $sshkeyexp && $sshkeyexp < 65537;
826   warn "*** Warning: The $keytype public key's prime divisor bit size "
827     . "($sshkeyexp) is weak (< 160), continuing anyway\n"
828     if !$quiet && $kk eq 'dsa' &&
829        $sshkeybits >= 1024 && $sshkeybits < 2048 && $sshkeyexp < 160;
830   warn "*** Warning: The $keytype public key's prime divisor bit size "
831     . "($sshkeyexp) is weak (< 224), continuing anyway\n"
832     if !$quiet && $kk eq 'dsa' &&
833        $sshkeybits >= 2048 && $sshkeybits < 3072 && $sshkeyexp < 224;
834   warn "*** Warning: The $keytype public key's prime divisor bit size "
835     . "($sshkeyexp) is weak (< 256), continuing anyway\n"
836     if !$quiet && $kk eq 'dsa' && $sshkeybits >= 3072 && $sshkeyexp < 256;
837
838   return 0 if $check;
839
840   if (!defined($format)) {
841     $format = $pubx509 ? 'ssh' : 'pem';
842   }
843
844   my $output;
845   if ($outfile ne '-') {
846     open($output, ">", $outfile)
847       or die "Cannot open \"$outfile\" for output: $!\n";
848   } else {
849     $output = *STDOUT;
850   }
851
852   if ($format eq 'ssh') {
853     my $cmnt = '';
854     $cmnt = ' ' . join(' ', @ARGV) if @ARGV;
855     print $output $dotpub, $cmnt, "\n";
856   } elsif ($format eq 'der') {
857     print $output $opensshpub;
858   } else {
859     my $base64 = join("\n", BreakLine(encode_base64($opensshpub, ''), 64))."\n";
860     print $output "-----BEGIN PUBLIC KEY-----\n",
861                   $base64,
862                   "-----END PUBLIC KEY-----\n";
863   }
864
865   close($output) if $outfile ne '-';
866   return 0;
867 }