X-Git-Url: https://codewiz.org/gitweb?a=blobdiff_plain;f=src%2Fshare%2Fkeytrans;h=171a1f64a910c0035ebea864fded44bbbf840b42;hb=d0116abc28011849b0de688200b8782d24088021;hp=c47ccdc792d6cdfd98cf6fdb591ee9dae13e3969;hpb=2be12861abfe70143aada51e9034a218967658bf;p=monkeysphere.git diff --git a/src/share/keytrans b/src/share/keytrans index c47ccdc..171a1f6 100755 --- a/src/share/keytrans +++ b/src/share/keytrans @@ -54,7 +54,7 @@ use File::Basename; use Crypt::OpenSSL::RSA; use Crypt::OpenSSL::Bignum; use Crypt::OpenSSL::Bignum::CTX; -use Digest::SHA1; +use Digest::SHA; use MIME::Base64; use POSIX; @@ -368,12 +368,12 @@ sub read_mpi { # FIXME: genericize these to accept either RSA or DSA keys: sub make_rsa_pub_key_body { my $key = shift; - my $timestamp = shift; + my $key_timestamp = shift; my ($n, $e) = $key->get_key_parameters(); return - pack('CN', 4, $timestamp). + pack('CN', 4, $key_timestamp). pack('C', $asym_algos->{rsa}). mpi_pack($n). mpi_pack($e); @@ -381,7 +381,7 @@ sub make_rsa_pub_key_body { sub make_rsa_sec_key_body { my $key = shift; - my $timestamp = shift; + my $key_timestamp = shift; # we're not using $a and $b, but we need them to get to $c. my ($n, $e, $d, $p, $q) = $key->get_key_parameters(); @@ -400,7 +400,7 @@ sub make_rsa_sec_key_body { # with modular_multi_inverse. return - pack('CN', 4, $timestamp). + pack('CN', 4, $key_timestamp). pack('C', $asym_algos->{rsa}). mpi_pack($n). mpi_pack($e). @@ -412,11 +412,11 @@ sub make_rsa_sec_key_body { # expects an RSA key (public or private) and a timestamp sub fingerprint { my $key = shift; - my $timestamp = shift; + my $key_timestamp = shift; - my $rsabody = make_rsa_pub_key_body($key, $timestamp); + my $rsabody = make_rsa_pub_key_body($key, $key_timestamp); - return Digest::SHA1::sha1(pack('Cn', 0x99, length($rsabody)).$rsabody); + return Digest::SHA::sha1(pack('Cn', 0x99, length($rsabody)).$rsabody); } @@ -426,42 +426,22 @@ sub pem2openpgp { my $uid = shift; my $args = shift; - $rsa->use_sha256_hash(); - - # see page 22 of RFC 4880 for why i think this is the right padding - # choice to use: - $rsa->use_pkcs1_padding(); - - if (! $rsa->check_key()) { - die "key does not check"; + # strong assertion of identity is the default (for a self-sig): + if (! defined $args->{certification_type}) { + $args->{certification_type} = $sig_types->{positive_certification}; } - my $version = pack('C', 4); - # strong assertion of identity: - my $sigtype = pack('C', $sig_types->{positive_certification}); - # RSA - my $pubkey_algo = pack('C', $asym_algos->{rsa}); - # SHA1 - my $hash_algo = pack('C', $digests->{sha256}); - - # FIXME: i'm worried about generating a bazillion new OpenPGP - # certificates from the same key, which could easily happen if you run - # this script more than once against the same key (because the - # timestamps will differ). How can we prevent this? - - # this environment variable (if set) overrides the current time, to - # be able to create a standard key? If we read the key from a file - # instead of stdin, should we use the creation time on the file? - my $timestamp = 0; - if (defined $args->{timestamp}) { - $timestamp = ($args->{timestamp} + 0); - } else { - $timestamp = time(); + if (! defined $args->{sig_timestamp}) { + $args->{sig_timestamp} = time(); } + if (! defined $args->{key_timestamp}) { + $args->{key_timestamp} = $args->{sig_timestamp} + 0; + } + my $key_timestamp = $args->{key_timestamp}; - my $creation_time_packet = pack('CCN', 5, $subpacket_types->{sig_creation_time}, $timestamp); - + # generate and aggregate subpackets: + # key usage flags: my $flags = 0; if (! defined $args->{usage_flags}) { $flags = $usage_flags->{certify}; @@ -474,17 +454,15 @@ sub pem2openpgp { $flags |= $usage_flags->{$f}; } } - - my $usage_packet = pack('CCC', 2, $subpacket_types->{usage_flags}, $flags); - + my $usage_subpacket = pack('CCC', 2, $subpacket_types->{usage_flags}, $flags); # how should we determine how far off to set the expiration date? # default is no expiration. Specify the timestamp in seconds from the # key creation. - my $expiration_packet = ''; + my $expiration_subpacket = ''; if (defined $args->{expiration}) { my $expires_in = $args->{expiration} + 0; - $expiration_packet = pack('CCN', 5, $subpacket_types->{key_expiration_time}, $expires_in); + $expiration_subpacket = pack('CCN', 5, $subpacket_types->{key_expiration_time}, $expires_in); } @@ -522,17 +500,71 @@ sub pem2openpgp { my $keyserver_pref = pack('CCC', 2, $subpacket_types->{keyserver_prefs}, $keyserver_prefs->{nomodify}); - my $subpackets_to_be_hashed = - $creation_time_packet. - $usage_packet. - $expiration_packet. + + $args->{hashed_subpackets} = + $usage_subpacket. + $expiration_subpacket. $pref_sym_algos. $pref_hash_algos. $pref_zip_algos. $feature_subpacket. $keyserver_pref; - my $subpacket_octets = pack('n', length($subpackets_to_be_hashed)); + return + make_packet($packet_types->{seckey}, make_rsa_sec_key_body($rsa, $key_timestamp)). + make_packet($packet_types->{uid}, $uid). + gensig($rsa, $uid, $args); +} + +# FIXME: handle non-RSA keys + +# FIXME: this currently only makes self-sigs -- we should parameterize +# it to make certifications over keys other than the issuer. +sub gensig { + my $rsa = shift; + my $uid = shift; + my $args = shift; + + # FIXME: allow signature creation using digests other than SHA256 + $rsa->use_sha256_hash(); + + # see page 22 of RFC 4880 for why i think this is the right padding + # choice to use: + $rsa->use_pkcs1_padding(); + + if (! $rsa->check_key()) { + die "key does not check"; + } + + my $certtype = $args->{certification_type} + 0; + + my $version = pack('C', 4); + my $sigtype = pack('C', $certtype); + # RSA + my $pubkey_algo = pack('C', $asym_algos->{rsa}); + # SHA256 FIXME: allow signature creation using digests other than SHA256 + my $hash_algo = pack('C', $digests->{sha256}); + + # FIXME: i'm worried about generating a bazillion new OpenPGP + # certificates from the same key, which could easily happen if you run + # this script more than once against the same key (because the + # timestamps will differ). How can we prevent this? + + # this argument (if set) overrides the current time, to + # be able to create a standard key. If we read the key from a file + # instead of stdin, should we use the creation time on the file? + my $sig_timestamp = ($args->{sig_timestamp} + 0); + my $key_timestamp = ($args->{key_timestamp} + 0); + + if ($key_timestamp > $sig_timestamp) { + die "key timestamp must not be later than signature timestamp"; + } + + my $creation_time_packet = pack('CCN', 5, $subpacket_types->{sig_creation_time}, $sig_timestamp); + + my $hashed_subs = $creation_time_packet.$args->{hashed_subpackets}; + + my $subpacket_octets = pack('n', length($hashed_subs)); my $sig_data_to_be_hashed = $version. @@ -540,10 +572,9 @@ sub pem2openpgp { $pubkey_algo. $hash_algo. $subpacket_octets. - $subpackets_to_be_hashed; + $hashed_subs; - my $pubkey = make_rsa_pub_key_body($rsa, $timestamp); - my $seckey = make_rsa_sec_key_body($rsa, $timestamp); + my $pubkey = make_rsa_pub_key_body($rsa, $key_timestamp); # this is for signing. it needs to be an old-style header with a # 2-packet octet count. @@ -551,7 +582,7 @@ sub pem2openpgp { my $key_data = make_packet($packet_types->{pubkey}, $pubkey, {'packet_length'=>2}); # take the last 8 bytes of the fingerprint as the keyid: - my $keyid = substr(fingerprint($rsa, $timestamp), 20 - 8, 8); + my $keyid = substr(fingerprint($rsa, $key_timestamp), 20 - 8, 8); # the v4 signature trailer is: @@ -569,7 +600,9 @@ sub pem2openpgp { $sig_data_to_be_hashed. $trailer; - my $data_hash = Digest::SHA1::sha1_hex($datatosign); + + # FIXME: handle signatures over digests other than SHA256: + my $data_hash = Digest::SHA::sha256_hex($datatosign); my $issuer_packet = pack('CCa8', 9, $subpacket_types->{issuer}, $keyid); @@ -582,14 +615,198 @@ sub pem2openpgp { pack('n', hex(substr($data_hash, 0, 4))). mpi_pack($sig); - return - make_packet($packet_types->{seckey}, $seckey). - make_packet($packet_types->{uid}, $uid). - make_packet($packet_types->{sig}, $sig_body); + return make_packet($packet_types->{sig}, $sig_body); +} + +# FIXME: switch to passing the whole packet as the arg, instead of the +# input stream. + +# FIXME: think about native perl representation of the packets instead. + +# Put a user ID into the $data +sub finduid { + my $data = shift; + my $instr = shift; + my $tag = shift; + my $packetlen = shift; + + my $dummy; + ($tag == $packet_types->{uid}) or die "This should not be called on anything but a User ID packet"; + + read($instr, $dummy, $packetlen); + $data->{uid} = {} unless defined $data->{uid}; + $data->{uid}->{$dummy} = {}; +} + + +# find signatures associated with the given fingerprint and user ID. +sub findsig { + my $data = shift; + my $instr = shift; + my $tag = shift; + my $packetlen = shift; + + ($tag == $packet_types->{sig}) or die "No calling findsig on anything other than a signature packet."; + + my $dummy; + my $readbytes = 0; + + if ((undef $data->{key}) || + (undef $data->{uid}) || + (undef $data->{uid}->{$data->{target}->{uid}})) { + # this is not the user ID we are looking for. + read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; + } + + read($instr, $data, 6) or die "could not read signature header\n"; + my ($ver, $sigtype, $pubkeyalgo, $digestalgo, $subpacketsize) = unpack('CCCCn', $data); + if ($ver != 4) { + printf(STDERR "We only work with version 4 signatures."); + read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; + return; + } + if ($pubkeyalgo != $asym_algos->{rsa}) { + printf(STDERR "We can only work with RSA at the moment"); + read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; + return; + } + if ($sigtype != $sig_types->{positive_certification}) { + # FIXME: some weird implementations might have made generic, + # persona, or casual certifications instead of positive + # certifications for self-sigs. Probably should handle them too. + read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; + return; + } + + my $subpackets; + read($instr, $subpackets, $subpacketsize) or die "could not read hashed signature subpackets.\n"; + + read($instr, $subpacketsize, 2) or die "could not read unhashed signature subpacket size.\n"; + $subpacketsize = unpack('n', $subpacketsize); + + my $unhashedsubpackets; + read($instr, $unhashedsubpackets, $subpacketsize) or die "could not read unhashed signature subpackets.\n"; + + my $hashtail; + read($instr, $hashtail, 2) or die "could not read left 16 bits of digest.\n"; + + # FIXME: RSA signatures should read in how many MPIs? + } +# given an input stream and data, store the found key in data and +# consume the rest of the stream corresponding to the packet. +# data contains: (fpr: fingerprint to find, key: current best guess at key) +sub findkey { + my $data = shift; + my $instr = shift; + my $tag = shift; + my $packetlen = shift; + + my $dummy; + my $ver; + my $readbytes = 0; + + read($instr, $ver, 1) or die "could not read key version\n"; + $readbytes += 1; + $ver = ord($ver); + + if ($ver != 4) { + printf(STDERR "We only work with version 4 keys. This key appears to be version %s.\n", $ver); + read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; + return; + } + + my $key_timestamp; + read($instr, $key_timestamp, 4) or die "could not read key timestamp.\n"; + $readbytes += 4; + $key_timestamp = unpack('N', $key_timestamp); + + my $algo; + read($instr, $algo, 1) or die "could not read key algorithm.\n"; + $readbytes += 1; + $algo = ord($algo); + if ($algo != $asym_algos->{rsa}) { + printf(STDERR "We only support RSA keys (this key used algorithm %d).\n", $algo); + read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; + return; + } + + ## we have an RSA key. + my $modulus = read_mpi($instr, \$readbytes); + my $exponent = read_mpi($instr, \$readbytes); + + my $pubkey = Crypt::OpenSSL::RSA->new_key_from_parameters($modulus, $exponent); + my $foundfpr = fingerprint($pubkey, $key_timestamp); + + my $foundfprstr = Crypt::OpenSSL::Bignum->new_from_bin($foundfpr)->to_hex(); + # left-pad with 0's to bring up to full 40-char (160-bit) fingerprint: + $foundfprstr = sprintf("%040s", $foundfprstr); + + # is this a match? + if ((!defined($data->{target}->{fpr})) || + (substr($foundfprstr, -1 * length($data->{target}->{fpr})) eq $data->{target}->{fpr})) { + if (defined($data->{key})) { + die "Found two matching keys.\n"; + } + $data->{key} = { 'rsa' => $pubkey, + 'timestamp' => $key_timestamp }; + } + + if ($tag != $packet_types->{seckey} && + $tag != $packet_types->{sec_subkey}) { + if ($readbytes < $packetlen) { + read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; + } + return; + } + if (!defined($data->{key})) { + # we don't think the public part of this key matches + if ($readbytes < $packetlen) { + read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; + } + return; + } + + my $s2k; + read($instr, $s2k, 1) or die "Could not read S2K octet.\n"; + $readbytes += 1; + $s2k = ord($s2k); + if ($s2k != 0) { + printf(STDERR "We cannot handle encrypted secret keys. Skipping!\n") ; + read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; + return; + } + + # secret material is unencrypted + # see http://tools.ietf.org/html/rfc4880#section-5.5.3 + my $d = read_mpi($instr, \$readbytes); + my $p = read_mpi($instr, \$readbytes); + my $q = read_mpi($instr, \$readbytes); + my $u = read_mpi($instr, \$readbytes); + + my $checksum; + read($instr, $checksum, 2) or die "Could not read checksum of secret key material.\n"; + $readbytes += 2; + $checksum = unpack('n', $checksum); -sub openpgp2ssh { + # FIXME: compare with the checksum! how? the data is + # gone into the Crypt::OpenSSL::Bignum + + $data->{key}->{rsa} = Crypt::OpenSSL::RSA->new_key_from_parameters($modulus, + $exponent, + $d, + $p, + $q); + + $data->{key}->{rsa}->check_key() or die "Secret key is not a valid RSA key.\n"; + + if ($readbytes < $packetlen) { + read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; + } +} + +sub openpgp2rsa { my $instr = shift; my $fpr = shift; @@ -600,12 +817,69 @@ sub openpgp2ssh { $fpr = uc($fpr); } + my $data = { 'fpr' => $fpr}; + my $subs = { $packet_types->{pubkey} => \&findkey, + $packet_types->{pub_subkey} => \&findkey, + $packet_types->{seckey} => \&findkey, + $packet_types->{sec_subkey} => \&findkey }; + + packetwalk($instr, $subs, $data); + + return $data->{key}->{rsa}; +} + +sub revokeuserid { + my $instr = shift; + my $fpr = shift; + my $uid = shift; + + if ((! defined $fpr) || + (length($fpr) < 8)) { + die "We need at least 8 hex digits of fingerprint.\n"; + } + + $fpr = uc($fpr); + + if (! defined $uid) { + die "No User ID defined.\n"; + } + + my $data = { target => { fpr => $fpr, + }, + }; + my $subs = { $packet_types->{seckey} => \&findkey, + $packet_types->{uid} => \&finduid + }; + + packetwalk($instr, $subs, $data); + + if ((! defined $data->{uid}) || + (! defined $data->{uid}->{$uid})) { + die "The User ID \"$uid\" is not associated with this key"; + } + + if ((! defined $data->{key}) || + (! defined $data->{key}->{rsa}) || + (! defined $data->{key}->{timestamp})) { + die "The key requested was not found." + } + + # what does a signature like this look like? + + return 'abc'; +} + + + +sub packetwalk { + my $instr = shift; + my $subs = shift; + my $data = shift; + my $packettag; my $dummy; my $tag; - my $key; - while (! eof($instr)) { read($instr, $packettag, 1); $packettag = ord($packettag); @@ -615,6 +889,7 @@ sub openpgp2ssh { die "This is not an OpenPGP packet\n"; } if (0x40 & $packettag) { + # this is a new-format packet. $tag = (0x3f & $packettag); my $nextlen = 0; read($instr, $nextlen, 1); @@ -633,6 +908,7 @@ sub openpgp2ssh { # packet length is undefined. } } else { + # this is an old-format packet. my $lentype; $lentype = 0x03 & $packettag; $tag = ( 0x3c & $packettag ) >> 2; @@ -654,102 +930,14 @@ sub openpgp2ssh { die "Undefined packet lengths are not supported.\n"; } - if ($tag == $packet_types->{pubkey} || - $tag == $packet_types->{pub_subkey} || - $tag == $packet_types->{seckey} || - $tag == $packet_types->{sec_subkey}) { - my $ver; - my $readbytes = 0; - read($instr, $ver, 1) or die "could not read key version\n"; - $readbytes += 1; - $ver = ord($ver); - - if ($ver != 4) { - printf(STDERR "We only work with version 4 keys. This key appears to be version %s.\n", $ver); - read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; - } else { - - my $timestamp; - read($instr, $timestamp, 4) or die "could not read key timestamp.\n"; - $readbytes += 4; - $timestamp = unpack('N', $timestamp); - - my $algo; - read($instr, $algo, 1) or die "could not read key algorithm.\n"; - $readbytes += 1; - $algo = ord($algo); - if ($algo != $asym_algos->{rsa}) { - printf(STDERR "We only support RSA keys (this key used algorithm %d).\n", $algo); - read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; - } else { - ## we have an RSA key. - my $modulus = read_mpi($instr, \$readbytes); - my $exponent = read_mpi($instr, \$readbytes); - - my $pubkey = Crypt::OpenSSL::RSA->new_key_from_parameters($modulus, $exponent); - my $foundfpr = fingerprint($pubkey, $timestamp); - - my $foundfprstr = Crypt::OpenSSL::Bignum->new_from_bin($foundfpr)->to_hex(); - # left-pad with 0's to bring up to full 40-char (160-bit) fingerprint: - $foundfprstr = sprintf("%040s", $foundfprstr); - - # is this a match? - if ((!defined($fpr)) || - (substr($foundfprstr, -1 * length($fpr)) eq $fpr)) { - if (defined($key)) { - die "Found two matching keys.\n"; - } - $key = $pubkey; - } - - if ($tag == $packet_types->{seckey} || - $tag == $packet_types->{sec_subkey}) { - if (!defined($key)) { # we don't think the public part of - # this key matches - read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; - } else { - my $s2k; - read($instr, $s2k, 1) or die "Could not read S2K octet.\n"; - $readbytes += 1; - $s2k = ord($s2k); - if ($s2k == 0) { - # secret material is unencrypted - # see http://tools.ietf.org/html/rfc4880#section-5.5.3 - my $d = read_mpi($instr, \$readbytes); - my $p = read_mpi($instr, \$readbytes); - my $q = read_mpi($instr, \$readbytes); - my $u = read_mpi($instr, \$readbytes); - - my $checksum; - read($instr, $checksum, 2) or die "Could not read checksum of secret key material.\n"; - $readbytes += 2; - $checksum = unpack('n', $checksum); - - # FIXME: compare with the checksum! how? the data is - # gone into the Crypt::OpenSSL::Bignum - - $key = Crypt::OpenSSL::RSA->new_key_from_parameters($modulus, - $exponent, - $d, - $p, - $q); - - $key->check_key() or die "Secret key is not a valid RSA key.\n"; - } else { - print(STDERR "We cannot handle encrypted secret keys. Skipping!\n") ; - read($instr, $dummy, $packetlen - $readbytes) or die "Could not skip past this packet.\n"; - } - } - } - - } - } + if (defined $subs->{$tag}) { + $subs->{$tag}($data, $instr, $tag, $packetlen); } else { read($instr, $dummy, $packetlen) or die "Could not skip past this packet!\n"; } } - return $key; + return $data->{key}; } @@ -764,7 +952,6 @@ for (basename($0)) { # FIXME: fail if there is no given user ID; or should we default to # hostname_long() from Sys::Hostname::Long ? - if (defined $ENV{PEM2OPENPGP_NEWKEY}) { $rsa = Crypt::OpenSSL::RSA->generate_key($ENV{PEM2OPENPGP_NEWKEY}); } else { @@ -778,7 +965,8 @@ for (basename($0)) { print pem2openpgp($rsa, $uid, - { timestamp => $ENV{PEM2OPENPGP_TIMESTAMP}, + { sig_timestamp => $ENV{PEM2OPENPGP_TIMESTAMP}, + key_timestamp => $ENV{PEM2OPENPGP_KEY_TIMESTAMP}, expiration => $ENV{PEM2OPENPGP_EXPIRATION}, usage_flags => $ENV{PEM2OPENPGP_USAGE_FLAGS}, } @@ -789,7 +977,7 @@ for (basename($0)) { my $instream; open($instream,'-'); binmode($instream, ":bytes"); - my $key = openpgp2ssh($instream, $fpr); + my $key = openpgp2rsa($instream, $fpr); if (defined($key)) { if ($key->is_private()) { print $key->get_private_key_string(); @@ -800,6 +988,17 @@ for (basename($0)) { die "No matching key found.\n"; } } + elsif (/^revokeuserid$/) { + my $fpr = shift; + my $uid = shift; + my $instream; + open($instream,'-'); + binmode($instream, ":bytes"); + + my $revcert = revokeuserid($instream, $fpr, $uid); + + print $revcert; + } else { die "Unrecognized keytrans call.\n"; }