CACreateCert: fix typo in help
[ezcert.git] / ConvertPubKey
1 #!/usr/bin/perl
2
3 # ConvertPubKey - Convert public keys to/from OpenSSH/X509 format
4 # Copyright (c) 2011,2012,2013 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.2';
39   $VERSIONMSG = "ConvertPubKey version $VERSION\n" .
40     "Copyright (c) 2011-2013 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 sub BreakLine($$)
610 {
611   my ($line,$width) = @_;
612   my @ans = ();
613   return $line if $width < 1;
614   while (length($line) > $width) {
615     push(@ans, substr($line, 0, $width));
616     substr($line, 0, $width) = '';
617   }
618   push(@ans, $line) if length($line);
619   return @ans;
620 }
621
622 sub main
623 {
624   my $help = '';
625   my $verbose = '';
626   my $quiet = '';
627   my $termOK = '';
628   my $debug = 0;
629   my $pubx509 = '';
630   my $check = '';
631   my $format = undef;
632   my $infile = '-';
633   my $outfile = '-';
634
635   #tests;
636   eval {GetOptions(
637       "help|h" => sub{$help=1;die"!FINISH"},
638       "verbose|v" => \$verbose,
639       "version|V" => sub{print STDERR $VERSIONMSG;exit(0)},
640       "debug" => \$debug,
641       "quiet" => \$quiet,
642       "pubx509" => \$pubx509,
643       "pubX509" => \$pubx509,
644       "check" => \$check,
645       "t" => \$termOK,
646       "format=s" => \$format,
647       "in=s" => \$infile,
648       "out=s" => \$outfile
649     )} || $help
650       or die $USAGE;
651   if ($help) {
652     local *MAN;
653     my $pager = $ENV{'PAGER'} || 'less';
654     if (-t STDOUT && open(MAN, "|-", $pager)) {
655       print MAN formatman($HELP,1);
656       close(MAN);
657     }
658     else {
659       print formatman($HELP);
660     }
661     exit(0);
662   }
663   die "--in requires a filename\n" if !$infile;
664   die "--out requires a filename\n" if !$outfile;
665   if (defined($format)) {
666     $format = lc($format);
667     die "--format argument must be 'ssh', 'pem' or 'der'\n"
668       if $format ne 'ssh' && $format ne 'pem' && $format ne 'der';
669   }
670   $verbose = 1 if $debug || $check;
671   print STDERR $VERSIONMSG if $verbose;
672   my $keytype = 'OpenSSH';
673   $keytype = 'X.509' if $pubx509;
674   die "Standard input is a tty (which is an unlikely source of a "
675     . "public key)\n"
676     . "If that's what you truly meant, add the -t option to allow it.\n"
677     if $infile eq '-' && -t STDIN && !$termOK;
678   my $opensshdotpub;
679   my $infilename;
680   {
681     local $/ if $pubx509;
682     my $input;
683     if ($infile ne '-') {
684       $infilename = "\"$infile\"";
685       open($input, '<', $infile)
686         or die "Cannot open $infilename for input: $!\n";
687     } else {
688       $input = *STDIN;
689       $infilename = 'standard input';
690     }
691     !!($opensshdotpub = <$input>)
692       or die "Cannot read $keytype public key from $infilename\n";
693     if (!$pubx509) {
694       my $auto509 = 0;
695       if ($opensshdotpub =~ /^----[- ]BEGIN PUBLIC KEY[- ]----/) {
696         $auto509 = 1;
697       }
698       else {
699         my $input = $opensshdotpub;
700         $input =~ s/((?:\r\n|\n|\r).*)$//os;
701         my @fields = split(' ', $input, 3);
702         if (@fields < 2 ||
703             length($fields[1]) < 16 ||
704             $fields[1] !~ m|^[0-9A-Za-z+/=]+$|) {
705           $auto509 = 1;
706         }
707       }
708       if ($auto509) {
709         $pubx509 = 1;
710         $keytype = 'X.509';
711         print STDERR "auto detected --pubx509 option\n" if $debug;
712         local $/;
713         my $extra = <$input>;
714         $opensshdotpub .= $extra if $extra;
715       }
716     }
717     close($input) if $infile ne '-';
718   }
719
720   my ($kk, $sshkeybits, $sshkeyexp, $sshkeyid, $sfmd5, $sfsha1, $sshcmnt);
721   my ($opensshpub, $dotpub);
722   if ($pubx509) {
723     my $inform =
724       ($opensshdotpub =~ m|^[\t\n\r\x20-\x7E\x80-\xFF]*$|os) ? 'PEM' : 'DER';
725     print STDERR "pubx509 -inform $inform\n" if $debug;
726     my $bin = 1;
727     if ($inform eq 'PEM') {
728       $opensshpub = $opensshdotpub;
729       if ($opensshpub =~ /^----[- ]BEGIN/m) {
730         $opensshpub = "\n".$opensshpub."\n";
731         $opensshpub =~ s/\A.*?\n----[- ]BEGIN[^\n]*\n//s;
732         $opensshpub =~ s/\n----[- ]END[^\n]*\n.*\z//s;
733         $opensshpub =~ tr/[ \t\r\n]//d;
734         $bin = 0;
735       } elsif ($opensshpub =~ m,\A[\t\r\n A-Za-z0-9+/=]*\z,s) {
736         $opensshpub =~ tr/[ \t\r\n]//d;
737         $bin = 0;
738       }
739     } else {
740       $opensshpub = $opensshdotpub;
741     }
742     print STDERR "BASE64 X509: $opensshpub\n" if !$bin && $debug;
743     $opensshpub = decode_base64($opensshpub) if !$bin;
744     print STDERR "HEX X509: ", unpack('H*', $opensshpub), "\n" if $debug;
745     $sshcmnt = undef;
746     ($kk,$sshkeybits,$sshkeyexp,$sshkeyid,$sfmd5,$sfsha1,$dotpub) =
747       GetKeyInfo($opensshpub);
748     die "Unparseable X.509 public key format read from $infilename\n"
749       unless $sshkeybits;
750     ($kk,$sshkeybits,$sshkeyexp,$sshkeyid,$sfmd5,$sfsha1,$sshcmnt,$opensshpub) =
751       GetOpenSSHKeyInfo($dotpub);
752   } else {
753     ($kk,$sshkeybits,$sshkeyexp,$sshkeyid,$sfmd5,$sfsha1,$sshcmnt,$opensshpub) =
754       GetOpenSSHKeyInfo($opensshdotpub);
755     die "Unparseable OpenSSH public key read from $infilename\n"
756       unless $sshkeybits;
757     ($kk,$sshkeybits,$sshkeyexp,$sshkeyid,$sfmd5,$sfsha1,$dotpub) =
758       GetKeyInfo($opensshpub);
759   }
760   my $keykind = uc($kk);
761   printf STDERR "$keytype $keykind Public Key Info:\n".
762       "  bits=$sshkeybits %s=$sshkeyexp\n", $kk eq 'rsa'?"pubexp":"divsiz" if $verbose;
763   print STDERR "  keyid=",
764     join(":", uc(unpack("H*",$sshkeyid))=~/../g), "\n" if $verbose;
765   print STDERR "  fingerprint(md5)=",
766     join(":", lc(unpack("H*",$sfmd5))=~/../g), "\n" if $verbose;
767   print STDERR "  fingerprint(sha1)=",
768     join(":", lc(unpack("H*",$sfsha1))=~/../g), "\n" if $verbose;
769   print STDERR "  comment=",$sshcmnt||'<none present>',"\n"
770     if $verbose && !$pubx509;
771   die "*** Error: $keytype key has less than 512 bits ($sshkeybits)\n"
772     . "***        You might as well just donate your system to hackers now.\n"
773     if $sshkeybits < 512;
774   die "*** Error: The $keytype key's public exponent is even ($sshkeyexp)!\n"
775     if $kk eq 'rsa' && $sshkeyexp && !($sshkeyexp & 0x01);
776   warn "*** Warning: The $keytype key has less than 2048 bits ($sshkeybits), "
777     . "continuing anyway\n" if !$quiet && $sshkeybits < 2048;
778   die "*** Error: The $keytype public key's exponent of $sshkeyexp is "
779     . "unacceptably weak!\n"
780     if $kk eq 'rsa' && $sshkeyexp && $sshkeyexp < 35; # OpenSSH used 35 until v5.4
781   warn "*** Warning: The $keytype public key's exponent ($sshkeyexp) is weak "
782     . "(< 65537), continuing anyway\n"
783     if !$quiet && $kk eq 'rsa' && $sshkeyexp && $sshkeyexp < 65537;
784   warn "*** Warning: The $keytype public key's prime divisor bit size "
785     . "($sshkeyexp) is weak (< 160), continuing anyway\n"
786     if !$quiet && $kk eq 'dsa' &&
787        $sshkeybits >= 1024 && $sshkeybits < 2048 && $sshkeyexp < 160;
788   warn "*** Warning: The $keytype public key's prime divisor bit size "
789     . "($sshkeyexp) is weak (< 224), continuing anyway\n"
790     if !$quiet && $kk eq 'dsa' &&
791        $sshkeybits >= 2048 && $sshkeybits < 3072 && $sshkeyexp < 224;
792   warn "*** Warning: The $keytype public key's prime divisor bit size "
793     . "($sshkeyexp) is weak (< 256), continuing anyway\n"
794     if !$quiet && $kk eq 'dsa' && $sshkeybits >= 3072 && $sshkeyexp < 256;
795
796   return 0 if $check;
797
798   if (!defined($format)) {
799     $format = $pubx509 ? 'ssh' : 'pem';
800   }
801
802   my $output;
803   if ($outfile ne '-') {
804     open($output, ">", $outfile)
805       or die "Cannot open \"$outfile\" for output: $!\n";
806   } else {
807     $output = *STDOUT;
808   }
809
810   if ($format eq 'ssh') {
811     my $cmnt = '';
812     $cmnt = ' ' . join(' ', @ARGV) if @ARGV;
813     print $output $dotpub, $cmnt, "\n";
814   } elsif ($format eq 'der') {
815     print $output $opensshpub;
816   } else {
817     my $base64 = join("\n", BreakLine(encode_base64($opensshpub, ''), 64))."\n";
818     print $output "-----BEGIN PUBLIC KEY-----\n",
819                   $base64,
820                   "-----END PUBLIC KEY-----\n";
821   }
822
823   close($output) if $outfile ne '-';
824   return 0;
825 }