From ef9469ee700eacfb9da0b2d897b82fbe1287e864 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 7 Jan 2009 02:17:03 -0500 Subject: added first pass at perl script to convert existing PEM-encoded RSA keys into OpenPGP keys --- src/keytrans/pem2openpgp | 180 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100755 src/keytrans/pem2openpgp (limited to 'src/keytrans/pem2openpgp') diff --git a/src/keytrans/pem2openpgp b/src/keytrans/pem2openpgp new file mode 100755 index 0000000..59f9bb0 --- /dev/null +++ b/src/keytrans/pem2openpgp @@ -0,0 +1,180 @@ +#!/usr/bin/perl -w -T + +# pem2openpgp: take a PEM-encoded RSA private-key on standard input, a +# User ID as the first argument, and generate an OpenPGP certificate +# from it. + +# Authors: +# Jameson Rollins +# Daniel Kahn Gillmor + +# Started on: 2009-01-07 02:01:19-0500 + +# License: GPL v3 or later (we may need to adjust this given that this +# connects to OpenSSL via perl) + +use strict; +use warnings; +use Crypt::OpenSSL::RSA; +use Crypt::OpenSSL::Bignum; +use Digest::SHA1; +use MIME::Base64; + +my $holdTerminator = $/; +undef $/; +my $buf = ; + + +my $rsa = Crypt::OpenSSL::RSA->new_private_key($buf); + +$rsa->use_sha1_hash(); +$rsa->use_no_padding(); + +if (! $rsa->check_key()) { + die "key does not check"; +} + +my $uid = 'fake key (do not use) '; + + + +my $version = pack('C', 4); +# strong assertion of identity: +my $sigtype = pack('C', 0x13); +# RSA +my $pubkey_algo = pack('C', 1); +# SHA1 +my $hash_algo = pack('C', 2); + + + +my $timestamp = 1231003584; + +my $creation_time_packet = pack('CCN', 5, 2, $timestamp); + + +# usage: signing and certification: +my $flags = 0x03; +my $usage_packet = pack('CCC', 2, 27, $flags); + + +# expire in 2 days: +my $expires_in = 86400*2; +my $expiration_packet = pack('CCN', 5, 9, $expires_in); + + +# prefer AES-256, AES-192, AES-128, CAST5, 3DES: +my $pref_sym_algos = pack('CCCCCCC', 6, 11, 9, 8, 7, 3, 2); + +# prefer SHA-1, SHA-256, RIPE-MD/160 +my $pref_hash_algos = pack('CCCCC', 4, 21, 2, 8, 3); + +# prefer ZLIB, BZip2, ZIP +my $pref_zip_algos = pack('CCCCC', 4, 22, 2, 3, 1); + +# we support the MDC feature: +my $features = pack('CCC', 2, 30, 1); + +# keyserver preference: only owner modify (???): +my $keyserver_pref = pack('CCC', 2, 23, 0x80); + +my $subpackets_to_be_hashed = + $creation_time_packet. + $usage_packet. + $expiration_packet. + $pref_sym_algos. + $pref_hash_algos. + $pref_zip_algos. + $features. + $keyserver_pref; + +#FIXME: what's the right way to get length()? +my $subpacket_octets = pack('n', length($subpackets_to_be_hashed)); + +my $sig_data_to_be_hashed = + $version. + $sigtype. + $pubkey_algo. + $hash_algo. + $subpacket_octets. + $subpackets_to_be_hashed; + + +my ($n, $e, $d, $p, $q) = $rsa->get_key_parameters(); + + +open(KEYFILE, "; + +# FIXME: $keyid should be generated from the public key instead of +# hardcoded: +my $keyid = '5616d7cb02e69446'; + +# the v4 signature trailer is: + +# version number, literal 0xff, and then a 4-byte count of the +# signature data itself. +my $trailer = pack('CCN', 4, 0xff, length($sig_data_to_be_hashed)); + +# FIXME: length() is probably not right here either in the event that +# the uid uses unicode. +my $uid_data = + pack('CN', 0xb4, length($uid)). + $uid; + +my $datatosign = + $key_data. + $uid_data. + $sig_data_to_be_hashed. + $trailer; + +my $data_hash = Digest::SHA1::sha1_hex($datatosign); + + +my $issuer_packet = pack('CCH16', 9, 16, $keyid); + +my $sig = $rsa->sign($datatosign); + +my $bigsig = Crypt::OpenSSL::Bignum->new_from_bin($sig); + + +my $hex = $bigsig->to_hex(); + +my $mpilen = length($hex)*4; + +# this is a kludgy way to get the number of bits in the first byte: +my $bitsinfirstbyte = length(sprintf("%b", hex(substr $hex, 0, 2))); + +$mpilen -= (8 - $bitsinfirstbyte); + +# emit two octets representing $mpilen, followed by the signature itself: + + +my $sig_body = + $sig_data_to_be_hashed. +# FIXME: another dubious length() call. + pack('n', length($issuer_packet)). + $issuer_packet. + pack('n', hex(substr($data_hash, 0, 4))). + pack("n" , $mpilen). + $sig; + +# FIXME: yet another length(): +my $len = length($sig_body); + +my $header; + +if ($len < 2**8) { + $header = pack('CC', 0x88, $len); +} elsif ($len < 2**16) { + $header = pack('Cn', 0x89, $len); +} elsif ($len < 2**31) { + $header = pack('CN', 0x8a, $len); +} else { + # what the hell do we do here? + $header = pack('C', 0x8b); +} + +print $header.$sig_body; + +$/ = $holdTerminator; -- cgit v1.2.3 From c2da43d48e1d8c54c089081d7f316bb925f426d9 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 7 Jan 2009 12:31:37 -0500 Subject: clean up a bit of pem2openpgp and remove some of the hardcoded data. --- src/keytrans/pem2openpgp | 102 ++++++++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 37 deletions(-) (limited to 'src/keytrans/pem2openpgp') diff --git a/src/keytrans/pem2openpgp b/src/keytrans/pem2openpgp index 59f9bb0..38baa95 100755 --- a/src/keytrans/pem2openpgp +++ b/src/keytrans/pem2openpgp @@ -20,6 +20,58 @@ use Crypt::OpenSSL::Bignum; use Digest::SHA1; use MIME::Base64; + +# make an old-style packet out of the given packet type and body. +# old-style (see RFC 4880 section 4.2) +sub make_packet { + my $type = shift; + my $body = shift; + +# FIXME: yet another length(): + my $len = length($body); + + my $lenbytes; + my $lencode; + + if ($len < 2**8) { + $lenbytes = 0; + $lencode = 'C'; + } elsif ($len < 2**16) { + $lenbytes = 1; + $lencode = 'n'; + } elsif ($len < 2**31) { + ## not testing against full 32 bits because i don't want to deal + ## with potential overflow. + $lenbytes = 2; + $lencode = 'N'; + } else { + ## what the hell do we do here? + $lenbytes = 3; + $lencode = ''; + } + + return pack('C'.$lencode, 0x80 + ($type * 4) + $lenbytes, $len). + $body; +} + + +# takes a Crypt::OpenSSL::Bignum +sub mpi_pack { + my $num = shift; + + my $hex = $num->to_hex(); + + my $mpilen = length($hex)*4; + +# this is a kludgy way to get the number of bits in the first byte: + my $bitsinfirstbyte = length(sprintf("%b", hex(substr $hex, 0, 2))); + + $mpilen -= (8 - $bitsinfirstbyte); + + return pack('n', $mpilen).$num->to_bin(); +} + + my $holdTerminator = $/; undef $/; my $buf = ; @@ -103,8 +155,14 @@ my $sig_data_to_be_hashed = my ($n, $e, $d, $p, $q) = $rsa->get_key_parameters(); -open(KEYFILE, "; +my $pubkey = + pack('CN', 4, $timestamp). + $pubkey_algo. + mpi_pack($n). + mpi_pack($e); + +#open(KEYFILE, "sign($datatosign); - -my $bigsig = Crypt::OpenSSL::Bignum->new_from_bin($sig); - - -my $hex = $bigsig->to_hex(); - -my $mpilen = length($hex)*4; - -# this is a kludgy way to get the number of bits in the first byte: -my $bitsinfirstbyte = length(sprintf("%b", hex(substr $hex, 0, 2))); - -$mpilen -= (8 - $bitsinfirstbyte); - -# emit two octets representing $mpilen, followed by the signature itself: - +my $sig = Crypt::OpenSSL::Bignum->new_from_bin($rsa->sign($datatosign)); my $sig_body = $sig_data_to_be_hashed. @@ -156,25 +199,10 @@ my $sig_body = pack('n', length($issuer_packet)). $issuer_packet. pack('n', hex(substr($data_hash, 0, 4))). - pack("n" , $mpilen). - $sig; - -# FIXME: yet another length(): -my $len = length($sig_body); - -my $header; - -if ($len < 2**8) { - $header = pack('CC', 0x88, $len); -} elsif ($len < 2**16) { - $header = pack('Cn', 0x89, $len); -} elsif ($len < 2**31) { - $header = pack('CN', 0x8a, $len); -} else { - # what the hell do we do here? - $header = pack('C', 0x8b); -} + mpi_pack($sig); -print $header.$sig_body; +print make_packet(6, $pubkey); +print make_packet(13, $uid); +print make_packet(2, $sig_body); $/ = $holdTerminator; -- cgit v1.2.3 From 099e48efe48e6d7f5bbc5ad61b5ed88c468623d2 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 7 Jan 2009 13:27:32 -0500 Subject: removed last hardcoded data in pem2openpgp; it seems to work with our test key. --- src/keytrans/pem2openpgp | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) (limited to 'src/keytrans/pem2openpgp') diff --git a/src/keytrans/pem2openpgp b/src/keytrans/pem2openpgp index 38baa95..1575671 100755 --- a/src/keytrans/pem2openpgp +++ b/src/keytrans/pem2openpgp @@ -20,6 +20,8 @@ use Crypt::OpenSSL::Bignum; use Digest::SHA1; use MIME::Base64; +## make sure all length() and substr() calls use bytes only: +use bytes; # make an old-style packet out of the given packet type and body. # old-style (see RFC 4880 section 4.2) @@ -55,7 +57,8 @@ sub make_packet { } -# takes a Crypt::OpenSSL::Bignum +# takes a Crypt::OpenSSL::Bignum, returns it formatted as OpenPGP MPI +# (RFC 4880 section 3.2) sub mpi_pack { my $num = shift; @@ -70,7 +73,30 @@ sub mpi_pack { return pack('n', $mpilen).$num->to_bin(); } +# FIXME: genericize this to accept either RSA or DSA keys: +sub make_rsa_key_body { + my $key = shift; + my $timestamp = shift; + my ($n, $e) = $key->get_key_parameters(); + + return + pack('CN', 4, $timestamp). + pack('C', 1). # RSA + mpi_pack($n). + mpi_pack($e); + +} + +# expects an RSA key (public or private) and a timestamp +sub fingerprint { + my $key = shift; + my $timestamp = shift; + + my $rsabody = make_rsa_key_body($key, $timestamp); + + return Digest::SHA1::sha1_hex(pack('Cn', 0x99, length($rsabody)).$rsabody); +} my $holdTerminator = $/; undef $/; @@ -130,7 +156,7 @@ my $features = pack('CCC', 2, 30, 1); # keyserver preference: only owner modify (???): my $keyserver_pref = pack('CCC', 2, 23, 0x80); -my $subpackets_to_be_hashed = +my $subpackets_to_be_hashed = $creation_time_packet. $usage_packet. $expiration_packet. @@ -151,22 +177,13 @@ my $sig_data_to_be_hashed = $subpacket_octets. $subpackets_to_be_hashed; - -my ($n, $e, $d, $p, $q) = $rsa->get_key_parameters(); - - -my $pubkey = - pack('CN', 4, $timestamp). - $pubkey_algo. - mpi_pack($n). - mpi_pack($e); +my $pubkey = make_rsa_key_body($rsa, $timestamp); #open(KEYFILE, " Date: Wed, 7 Jan 2009 13:35:17 -0500 Subject: use bytes in pem2openpgp to ensure that length calculations are done by octet and not by character. --- src/keytrans/pem2openpgp | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) (limited to 'src/keytrans/pem2openpgp') diff --git a/src/keytrans/pem2openpgp b/src/keytrans/pem2openpgp index 1575671..f6e2d4f 100755 --- a/src/keytrans/pem2openpgp +++ b/src/keytrans/pem2openpgp @@ -29,7 +29,6 @@ sub make_packet { my $type = shift; my $body = shift; -# FIXME: yet another length(): my $len = length($body); my $lenbytes; @@ -62,17 +61,18 @@ sub make_packet { sub mpi_pack { my $num = shift; - my $hex = $num->to_hex(); + my $val = $num->to_bin(); + my $mpilen = length($val)*8; - my $mpilen = length($hex)*4; - -# this is a kludgy way to get the number of bits in the first byte: - my $bitsinfirstbyte = length(sprintf("%b", hex(substr $hex, 0, 2))); +# this is a kludgy way to get the number of significant bits in the +# first byte: + my $bitsinfirstbyte = length(sprintf("%b", ord($val))); $mpilen -= (8 - $bitsinfirstbyte); - return pack('n', $mpilen).$num->to_bin(); + return pack('n', $mpilen).$val; } + # FIXME: genericize this to accept either RSA or DSA keys: sub make_rsa_key_body { my $key = shift; @@ -166,7 +166,6 @@ my $subpackets_to_be_hashed = $features. $keyserver_pref; -#FIXME: what's the right way to get length()? my $subpacket_octets = pack('n', length($subpackets_to_be_hashed)); my $sig_data_to_be_hashed = @@ -191,8 +190,6 @@ my $keyid = substr(fingerprint($rsa, $timestamp), 40 - 16, 16); # signature data itself. my $trailer = pack('CCN', 4, 0xff, length($sig_data_to_be_hashed)); -# FIXME: length() is probably not right here either in the event that -# the uid uses unicode. my $uid_data = pack('CN', 0xb4, length($uid)). $uid; @@ -212,7 +209,6 @@ my $sig = Crypt::OpenSSL::Bignum->new_from_bin($rsa->sign($datatosign)); my $sig_body = $sig_data_to_be_hashed. -# FIXME: another dubious length() call. pack('n', length($issuer_packet)). $issuer_packet. pack('n', hex(substr($data_hash, 0, 4))). -- cgit v1.2.3 From ad8c2c433a163b9b29281b80fb1390bfcd9756bd Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 7 Jan 2009 14:59:40 -0500 Subject: pem2openpgp now accepts a choice of User ID on stdin. --- src/keytrans/pem2openpgp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/keytrans/pem2openpgp') diff --git a/src/keytrans/pem2openpgp b/src/keytrans/pem2openpgp index f6e2d4f..3fdc469 100755 --- a/src/keytrans/pem2openpgp +++ b/src/keytrans/pem2openpgp @@ -23,6 +23,10 @@ use MIME::Base64; ## make sure all length() and substr() calls use bytes only: use bytes; +my $uid = shift; + +# FIXME: fail if there is no given user ID. + # make an old-style packet out of the given packet type and body. # old-style (see RFC 4880 section 4.2) sub make_packet { @@ -112,10 +116,6 @@ if (! $rsa->check_key()) { die "key does not check"; } -my $uid = 'fake key (do not use) '; - - - my $version = pack('C', 4); # strong assertion of identity: my $sigtype = pack('C', 0x13); -- cgit v1.2.3 From c71c0212bc36ed18d6df60c7a1dc0c3f6c541339 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 7 Jan 2009 15:02:05 -0500 Subject: clarifying make_rsa_key_body() to make_rsa_pub_key_body() --- src/keytrans/pem2openpgp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/keytrans/pem2openpgp') diff --git a/src/keytrans/pem2openpgp b/src/keytrans/pem2openpgp index 3fdc469..c5277cd 100755 --- a/src/keytrans/pem2openpgp +++ b/src/keytrans/pem2openpgp @@ -78,7 +78,7 @@ sub mpi_pack { } # FIXME: genericize this to accept either RSA or DSA keys: -sub make_rsa_key_body { +sub make_rsa_pub_key_body { my $key = shift; my $timestamp = shift; @@ -97,7 +97,7 @@ sub fingerprint { my $key = shift; my $timestamp = shift; - my $rsabody = make_rsa_key_body($key, $timestamp); + my $rsabody = make_rsa_pub_key_body($key, $timestamp); return Digest::SHA1::sha1_hex(pack('Cn', 0x99, length($rsabody)).$rsabody); } @@ -176,7 +176,7 @@ my $sig_data_to_be_hashed = $subpacket_octets. $subpackets_to_be_hashed; -my $pubkey = make_rsa_key_body($rsa, $timestamp); +my $pubkey = make_rsa_pub_key_body($rsa, $timestamp); #open(KEYFILE, " Date: Wed, 7 Jan 2009 15:46:19 -0500 Subject: pem2openpgp: clean up comments, treat fingerprint as raw data instead of ascii --- src/keytrans/pem2openpgp | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) (limited to 'src/keytrans/pem2openpgp') diff --git a/src/keytrans/pem2openpgp b/src/keytrans/pem2openpgp index c5277cd..7522c8f 100755 --- a/src/keytrans/pem2openpgp +++ b/src/keytrans/pem2openpgp @@ -4,6 +4,10 @@ # User ID as the first argument, and generate an OpenPGP certificate # from it. +# Usage: + +# pem2openpgp 'ssh://'$(hostname -f) < /etc/ssh/ssh_host_rsa_key | gpg --import + # Authors: # Jameson Rollins # Daniel Kahn Gillmor @@ -25,7 +29,8 @@ use bytes; my $uid = shift; -# FIXME: fail if there is no given user ID. +# FIXME: fail if there is no given user ID; or should we default to +# hostname_long() from Sys::Hostname::Long ? # make an old-style packet out of the given packet type and body. # old-style (see RFC 4880 section 4.2) @@ -99,10 +104,11 @@ sub fingerprint { my $rsabody = make_rsa_pub_key_body($key, $timestamp); - return Digest::SHA1::sha1_hex(pack('Cn', 0x99, length($rsabody)).$rsabody); + return Digest::SHA1::sha1(pack('Cn', 0x99, length($rsabody)).$rsabody); } -my $holdTerminator = $/; +# we're just not dealing with newline business right now. slurp in +# the whole file. undef $/; my $buf = ; @@ -124,9 +130,13 @@ my $pubkey_algo = pack('C', 1); # SHA1 my $hash_algo = pack('C', 2); +# 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. How can we prevent +# this? - -my $timestamp = 1231003584; +# could an environment variable (if set) override the current time? +my $timestamp = time(); my $creation_time_packet = pack('CCN', 5, 2, $timestamp); @@ -136,7 +146,9 @@ my $flags = 0x03; my $usage_packet = pack('CCC', 2, 27, $flags); -# expire in 2 days: +# FIXME: HARDCODED: how should we determine how far off to set the +# expiration date? default is to expire in 2 days, which is insanely +# short (but good for testing). my $expires_in = 86400*2; my $expiration_packet = pack('CCN', 5, 9, $expires_in); @@ -181,8 +193,8 @@ my $pubkey = make_rsa_pub_key_body($rsa, $timestamp); #open(KEYFILE, "new_from_bin($rsa->sign($datatosign)); @@ -214,8 +226,9 @@ my $sig_body = pack('n', hex(substr($data_hash, 0, 4))). mpi_pack($sig); -print make_packet(6, $pubkey); -print make_packet(13, $uid); -print make_packet(2, $sig_body); +print + make_packet(6, $pubkey). + make_packet(13, $uid). + make_packet(2, $sig_body); + -$/ = $holdTerminator; -- cgit v1.2.3 From f8344402aebe5f0497a81934b980b9ed6ea7a6a2 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 7 Jan 2009 16:17:49 -0500 Subject: pem2openpgp: break out usage flags, default to creating an authentication-capable primary key. --- src/keytrans/pem2openpgp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) (limited to 'src/keytrans/pem2openpgp') diff --git a/src/keytrans/pem2openpgp b/src/keytrans/pem2openpgp index 7522c8f..2fa221d 100755 --- a/src/keytrans/pem2openpgp +++ b/src/keytrans/pem2openpgp @@ -107,6 +107,23 @@ sub fingerprint { return Digest::SHA1::sha1(pack('Cn', 0x99, length($rsabody)).$rsabody); } +# FIXME: make tables of relevant identifiers: digest algorithms, +# ciphers, asymmetric crypto, packet types, subpacket types, signature +# types. As these are created, replace the opaque numbers below with +# semantically-meaningful code. + +# see RFC 4880 section 5.2.3.21 +my $usage_flags = { certify => 0x01, + sign => 0x02, + encrypt_comms => 0x04, + encrypt_storage => 0x08, + encrypt => 0x0c, ## both comms and storage + split => 0x10, # the private key is split via secret sharing + authenticate => 0x20, + shared => 0x80, # more than one person holds the entire private key + }; + + # we're just not dealing with newline business right now. slurp in # the whole file. undef $/; @@ -141,8 +158,9 @@ my $timestamp = time(); my $creation_time_packet = pack('CCN', 5, 2, $timestamp); -# usage: signing and certification: -my $flags = 0x03; +# FIXME: HARDCODED: what if someone wants to select a different set of +# usage flags? For now, we do only authentication. +my $flags = $usage_flags->{authenticate}; my $usage_packet = pack('CCC', 2, 27, $flags); -- cgit v1.2.3