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 [↩]