Convert a TMSR key to PGP

29/05/18, modified 17/09/18

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;

  1. 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 []

Leave a Reply