A script is floating around to convert TMSR key format (e,n,comment) to a PGP key for digesting in phuctor. This script did not work on the machines I tried it on. Of course, the script is fine, it's PGPY that is broken. I could not get it to install. As I'm programming in python for a living and have all kinds of stupid in me, I decided to try and fix the pgpy code that failed to install. An hour was so spent and some material gathered for a future blog post, but not any working code1.
After that I decided to spent another hour making an alternative that uses only standard python modules. I read RFC 4880 a month ago, this left me with headache back then. The thing is unreadable. So to make this script, I made extensive use of the search function in my browser and only read those lines that helped in writing the script.
The script;
import struct import time import sys import base64 import math # some format strings for the struct module # these are used to encode integers and shorts to arrays of bytes # '>' stands for big-endian as this is what is used in the PGP format openpgp_publickey_format = ">BIB" mpi_format = ">H" packet_length_format = ">I" crc_format = ">I" # determine the index of the highest bit set to 1 in a number def count_bits(B): R = 0 i = 0 while B > 0: i += 1 if B & 0x1: R = i B >>= 1 return R # Convert a number to an array of bytes # The bytes in the array are stored in big-endian order. # The most significant byte is stored as the first item # in the array def number_to_bytes(B): R = [] bits = 0 while B > 0xff: bits += 8 R.append(B & 0xff) B >>= 8 R.append(B) bits += count_bits(B) return bits, ''.join(map(chr, reversed(R))) # An MPI is a byte array that starts with a two byte # length header. The length is given in bits. def number_to_mpi(B): C, A = number_to_bytes(B) return struct.pack(mpi_format, C) + A # A PGP public key header consists of a byte "4", # an integer (4 bytes) to denote the timestamp # and a byte "1" (RSA). def public_key_header(T): return struct.pack(openpgp_publickey_format, 4, T, 1) # A public key packet is the public key header # plus 2 MPI numbers, the RSA modulus (N) and # the RSA exponent (e). def public_key_packet(t, n, e): return ''.join((public_key_header(t), number_to_mpi(n), number_to_mpi(e),)) # A comment or userid packet is a string encoded as utf-8 def userid_packet(s): return s.encode('utf8') # The PGP format is a stream of "packets". # Each packet has a header. This header consists of a tag # and a length field. The tag has flags to determine if it is a # "new" or "old" packet. # The only supported encoding in this scriptis "new". def encode_packet(packet_bytes, tag = 6): # 0x80, 8th bit always set, 7th bit set --> new packet h = 0x80 | 0x40 # 0-5 bits -> the tag h |= tag # convert the integer to a byte header = chr(h) # dude, this is totally how you may save 2 or 3 bytes with minimal complexity l = len(packet_bytes) if l < 192: header += chr(l) elif l < 8384: l -= 192 o1 = l >> 0xff o2 = l & 0xff header += chr(o1 + 192) + chr(o2) else: header += chr(0xff) + struct.pack(packet_length_format, l) return header + packet_bytes # When you encode binary data as an ascii text with base64 # this data becomes fragile. So a CRC code is needed to # fix this. def crc24(s): R = 0xB704CE for char in s: B = ord(char) R ^= B << 16 for i in range(8): R <<= 1; if R & 0x1000000: R ^= 0x1864CFB return R & 0xFFFFFF # Create a public key for consumption by Phuctor. # The public key needs to contain 2 packets # one for the key data (n, e) # one for the comment # It must be in the armor / ascii format. def enarmored_public_key(n, e, comment, t): R = [] # the header R.append("-----BEGIN PGP PUBLIC KEY BLOCK-----") R.append("") # the packets in bytes A = encode_packet(public_key_packet(t, n, e), 6) A += encode_packet(userid_packet(comment), 13) # the packets in base64 encoding with line length max 76 s=base64.b64encode(A) i = 0 while i < len(s): R.append(s[i:i+76]) i += 76 # the CRC R.append("="+base64.b64encode(struct.pack(crc_format, crc24(A))[1:])) # the footer R.append("") R.append("-----END PGP PUBLIC KEY BLOCK-----") return 'n'.join(R) # read a file with comma separated lines # each line is in the TMSR format: e,n,comment if __name__ == "__main__": ser = 1 for x in sys.stdin: x = x.strip() # ignore empty lines if len(x) == 0 or x.startswith('#'): continue # the comment may contain comma's so split on the first 2 e,n,comment = x.split(',', 2) t0 = int(time.time()) with open("{0}.txt".format(ser), "wb") as stream: stream.write(enarmored_public_key(int(n), int(e), comment, t0)) ser += 1
And the patch itself with signature;
- The patch: tmsr-pgp-genesis.vpatch.
- A signature: tmsr-pgp-genesis.vpatch.ave1.sig.
- I've been reading code (both open and closed source) for a large part of my life. I started this whole career by typing over basic programs into my fathers Commodore 128 and then stumbled along. The code I read in these popular security programs (pgpy, openssl, openssh, pgp) is markedly worse than any I encountered before. I can only image the kind of cockroaches that are attracted to this foul smelling mess [↩]