README: add some headings and .md alias
[ezcert.git] / ConvertPubKey
Commit [+]AuthorDateLineData
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -07001#!/usr/bin/perl
2
3# ConvertPubKey - Convert public keys to/from OpenSSH/X509 format
b013d07c Kyle J. McKay2014-10-28 14:21:09 -07004# Copyright (c) 2011,2012,2013,2014 Kyle J. McKay. All rights reserved.
79b0cd22 Kyle J. McKay2013-06-11 10:50:28 -07005
d3da30b5
KM
Kyle J. McKay2013-06-19 22:36:56 -07006# *** See detailed help starting around line 80 ***
7
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -07008# 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
21exit(&main());
22
23use strict;
24use warnings;
25use bytes;
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -070026
27use MIME::Base64;
28use IPC::Open2;
29use Digest::MD5 qw(md5 md5_hex md5_base64);
30use Getopt::Long qw(:config gnu_getopt);
31
32our $VERSION;
33my $VERSIONMSG;
34my $HELP;
35my $USAGE;
36
37BEGIN {
6ae6da5d Kyle J. McKay2014-11-01 18:41:46 -070038 *VERSION = \'1.0.4';
79b0cd22 Kyle J. McKay2013-06-11 10:50:28 -070039 $VERSIONMSG = "ConvertPubKey version $VERSION\n" .
b013d07c Kyle J. McKay2014-10-28 14:21:09 -070040 "Copyright (c) 2011-2014 Kyle J. McKay. All rights reserved.\n" .
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -070041 "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
47BEGIN {
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
73BEGIN {
74 $USAGE = <<'USAGE';
75Usage: 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]
78USAGE
79 $HELP = <<'HELP';
80NAME
81 ConvertPubKey -- convert public key formats
82
83SYNOPSIS
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
88DESCRIPTION
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
107OPTIONS
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
165NOTES
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
173TIPS
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
184HELP
185}
186
187sub 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
197sub 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
207sub 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
228sub DERLength($)
229{
230 # return a DER encoded length
231 my $len = shift;
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700232 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
240sub 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
255sub 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
271sub 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
290sub 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
309sub 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
318sub 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
2791b06c Kyle J. McKay2013-09-10 09:01:08 -0700324 # the public exponent (if RSA) or how many bits in prime divisor (if DSA)
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700325 # 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
2791b06c
KM
Kyle J. McKay2013-09-10 09:01:08 -0700350 # 4 Byte Big-Endian length of prime divisor q
351 # Prime divisor q integer bytes
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700352 # 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
2791b06c Kyle J. McKay2013-09-10 09:01:08 -0700415 PackUIntBytes($parts[2]). # prime divisor q
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700416 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;
2791b06c
KM
Kyle J. McKay2013-09-10 09:01:08 -0700429 my $divsiz = CountKeyBits($parts[2]);
430 return undef unless $divsiz;
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700431
432 my $cmnt = $fields[2]||'';
433 return
2791b06c Kyle J. McKay2013-09-10 09:01:08 -0700434 ('dsa',$bits,$divsiz,$id,md5($origData),sha1($origData),$cmnt,$pubdsa);
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700435 }
436}
437
438sub 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
2791b06c Kyle J. McKay2013-09-10 09:01:08 -0700444 # the public exponent (if RSA) or how many bits in prime divisor (if DSA)
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700445 # 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
2791b06c Kyle J. McKay2013-09-10 09:01:08 -0700472 # INTEGER prime divisor q
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700473 # 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;
2791b06c
KM
Kyle J. McKay2013-09-10 09:01:08 -0700597 my $divsiz = CountKeyBits($dsa_q);
598 return undef unless $divsiz;
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700599
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
2791b06c Kyle J. McKay2013-09-10 09:01:08 -0700605 return ('dsa',$bits,$divsiz,$id,md5($sshbin),sha1($sshbin),$sshpub);
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700606 }
607}
608
b013d07c
KM
Kyle J. McKay2014-10-28 14:21:09 -0700609my %rsadsa_known_strengths;
610BEGIN {
611 %rsadsa_known_strengths = (
612 1024 => 80,
613 2048 => 112,
614 3072 => 128,
615 7680 => 192,
616 15360 => 256,
617 );
618}
619
620sub 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;
6ae6da5d Kyle J. McKay2014-11-01 18:41:46 -0700635 $guess = 80 if $rsadsabits > 1024 && $guess < 80;
b013d07c Kyle J. McKay2014-10-28 14:21:09 -0700636 $guess = 111 if $rsadsabits > 1024 && $rsadsabits < 2048 && $guess >= 112;
6ae6da5d Kyle J. McKay2014-11-01 18:41:46 -0700637 $guess = 112 if $rsadsabits > 2048 && $guess < 112;
b013d07c Kyle J. McKay2014-10-28 14:21:09 -0700638 $guess = 127 if $rsadsabits > 2048 && $rsadsabits < 3072 && $guess >= 128;
6ae6da5d Kyle J. McKay2014-11-01 18:41:46 -0700639 $guess = 128 if $rsadsabits > 3072 && $guess < 128;
b013d07c Kyle J. McKay2014-10-28 14:21:09 -0700640 $guess = 191 if $rsadsabits > 3072 && $rsadsabits < 7680 && $guess >= 192;
6ae6da5d Kyle J. McKay2014-11-01 18:41:46 -0700641 $guess = 192 if $rsadsabits > 7680 && $guess < 192;
b013d07c Kyle J. McKay2014-10-28 14:21:09 -0700642 $guess = 255 if $rsadsabits > 7680 && $rsadsabits < 15360 && $guess >= 256;
6ae6da5d Kyle J. McKay2014-11-01 18:41:46 -0700643 $guess = 256 if $rsadsabits > 15360 && $guess < 256;
b013d07c
KM
Kyle J. McKay2014-10-28 14:21:09 -0700644 return int($guess) . ' (approximately)';
645}
646
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700647sub 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
660sub 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,
1755bfc9
KM
Kyle J. McKay2013-09-10 07:52:47 -0700685 "in=s" => \$infile,
686 "out=s" => \$outfile
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700687 )} || $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);
b013d07c
KM
Kyle J. McKay2014-10-28 14:21:09 -0700799 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 }
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700805 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"
2791b06c Kyle J. McKay2013-09-10 09:01:08 -0700817 if $kk eq 'rsa' && $sshkeyexp && !($sshkeyexp & 0x01);
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700818 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"
2791b06c Kyle J. McKay2013-09-10 09:01:08 -0700822 if $kk eq 'rsa' && $sshkeyexp && $sshkeyexp < 35; # OpenSSH used 35 until v5.4
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700823 warn "*** Warning: The $keytype public key's exponent ($sshkeyexp) is weak "
824 . "(< 65537), continuing anyway\n"
2791b06c
KM
Kyle J. McKay2013-09-10 09:01:08 -0700825 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;
79b0cd22
KM
Kyle J. McKay2013-06-11 10:50:28 -0700837
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}