summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKali Kaneko <kali@futeisha.org>2015-09-22 23:46:13 -0400
committerKali Kaneko <kali@futeisha.org>2015-09-25 12:00:48 -0400
commit2826d7a9300ad75672bee1b8aea6459d6f1cdb3e (patch)
treefaaca8c03aad2e9e0de5ee97a84795cd08fc6983
initial commit
-rw-r--r--.gitignore65
-rw-r--r--README3
-rw-r--r--memoryhole/__init__.py0
-rw-r--r--memoryhole/memoryhole.py258
-rw-r--r--requirements.pip2
-rw-r--r--setup.py0
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/corpus/OpenPGP/.gitignore1
-rw-r--r--tests/corpus/OpenPGP/README.md4
-rwxr-xr-xtests/corpus/OpenPGP/genkeys31
-rwxr-xr-xtests/corpus/OpenPGP/gnupg-import27
-rw-r--r--tests/corpus/OpenPGP/julia.key59
-rw-r--r--tests/corpus/OpenPGP/julia.pgp30
-rw-r--r--tests/corpus/OpenPGP/obrian.key59
-rw-r--r--tests/corpus/OpenPGP/obrian.pgp30
-rw-r--r--tests/corpus/OpenPGP/winston.key59
-rw-r--r--tests/corpus/OpenPGP/winston.pgp30
-rw-r--r--tests/corpus/expected.A.eml69
-rw-r--r--tests/corpus/sample.A.eml32
19 files changed, 759 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..916d8e6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,65 @@
+########## Generated by gig ###########
+
+### Python ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# swapfiles
+*.swp
+*.swo
diff --git a/README b/README
new file mode 100644
index 0000000..1cabddc
--- /dev/null
+++ b/README
@@ -0,0 +1,3 @@
+Protected E-mail headers.
+
+http://modernpgp.org/memoryhole/
diff --git a/memoryhole/__init__.py b/memoryhole/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/memoryhole/__init__.py
diff --git a/memoryhole/memoryhole.py b/memoryhole/memoryhole.py
new file mode 100644
index 0000000..84d607c
--- /dev/null
+++ b/memoryhole/memoryhole.py
@@ -0,0 +1,258 @@
+import logging
+import tempfile
+
+import email
+from email.message import Message
+from email.header import Header
+from email.MIMEMultipart import MIMEMultipart
+from email.parser import Parser
+from StringIO import StringIO
+
+import gnupg
+
+logger = logging.getLogger(__name__)
+
+COPY_HEADERS = ('Subject', 'Message-ID', 'Date', 'To', 'From')
+
+
+class ProtectionLevel(object):
+
+ def __init__(self, signed_by, encrypted_by):
+
+ self.signed_by = signed_by
+ self.encrypted_by = encrypted_by
+
+ # TODO add __cmp__
+
+ @property
+ def score(self):
+ if self.signed_by and self.encrypted_by:
+ return 3
+ elif self.signed_by:
+ return 2
+ elif self.encrypted_by:
+ return 1
+ else:
+ return 0
+
+ def __repr__(self):
+ return '<ProtectionLevel: sig(%s) encr(%s) score:%s>' % (
+ len(self.signed_by), len(self.encrypted_by), self.score)
+
+
+class MemoryHoleHeader(Header):
+
+ def __init__(self, name, value):
+ self._name = name
+ self._value = value
+
+ self._h = Header(value, header_name=name)
+
+ self.signed_by = set([])
+ self.encrypted_by = set([])
+
+ self._firstlinelen = self._h._firstlinelen
+ self._chunks = self._h._chunks
+ self._continuation_ws = self._h._continuation_ws
+ self._charset = self._h._charset
+
+ @property
+ def protection_level(self):
+ return ProtectionLevel(self.signed_by, self.encrypted_by)
+
+ def __repr__(self):
+ return '<MemoryHoleHeader(%s) [%s: %s]>' % (
+ self.protection_level.score, self._name, self._value)
+
+
+class MemoryHoleMessage(Message):
+
+ def __init__(self, msg, gpg):
+ self._msg = msg
+ self._gpg = gpg
+
+ verified = verify_signature(msg, gpg)
+ self.signed = verified.valid
+
+ self._mh_headers = {}
+ inner_headers = extract_wrapped_headers(msg)
+
+ for name, value in inner_headers.items():
+ mhh = MemoryHoleHeader(name, value)
+ mhh.signed_by.add(verified.key_id)
+ self._mh_headers[name] = mhh
+
+ self._charset = self._msg._charset
+ self._headers = self._msg._headers
+ self._payload = self._msg._payload
+ self.preamble = self._msg.preamble
+
+ def get_protected_header(self, header_name):
+ return self._mh_headers.get(header_name)
+
+ # TODO add is_tampered_header
+ # TODO add __getattr__, lookup the protected headers first
+
+
+def extract_wrapped_headers(msg):
+ top_payload = msg.get_payload()[0]
+ if top_payload.get_content_type() == 'multipart/mixed':
+ headers = _extract_attached_headers(top_payload)
+ return headers
+
+
+def verify_signature(msg, gpg):
+ if not msg.get_content_type() == 'multipart/signed':
+ logger.error('attempted to verify signature, but msg is not signed')
+ return False
+ payloads = msg.get_payload()
+ body = payloads[0]
+ attached_sig = payloads[1]
+
+ if not attached_sig.get_content_type() == 'application/pgp-signature':
+ logger.error('expected an attached signature')
+ return False
+ sig = _extract_sig_block(attached_sig.as_string())
+ fd, sig_fname = tempfile.mkstemp()
+ with open(sig_fname, 'w') as sigfile:
+ sigfile.write(sig)
+
+ body = body.as_string().replace('\n', '\r\n')
+ verified = gpg.verify_file(StringIO(body), sig_file=sig_fname)
+ return verified
+
+
+def protect_message(basemsg, gpg, do_sign=True, boundary=None,
+ sign_digest_algo='SHA512', passphrase=None):
+
+ msg = email.message.Message()
+ _msg = MIMEMultipart(
+ _subtype="signed", micalg="pgp-%s" % sign_digest_algo.lower(),
+ protocol="application/pgp-signature")
+
+ for hdr in COPY_HEADERS:
+ msg[hdr] = basemsg[hdr]
+ for hdr in _msg.keys():
+ msg[hdr] = _msg[hdr]
+ if boundary:
+ msg.set_boundary(boundary())
+
+ wrapper = _wrap_with_header(basemsg, boundary=boundary)
+ msg.attach(wrapper)
+
+ if do_sign:
+ payload = wrapper.as_string().replace('\n', '\r\n')
+ sig = gpg.sign(
+ payload, detach=True,
+ digest_algo=sign_digest_algo, clearsign=False,
+ passphrase=passphrase)
+
+ signed_msg = _make_signature_subpart(sig)
+ msg.attach(signed_msg)
+ return msg
+
+
+def _extract_sig_block(text):
+ sep = '\n'
+ BEGIN = '-----BEGIN PGP SIGNATURE-----'
+ sig_lines = text.split(sep)
+ return sep.join(sig_lines[sig_lines.index(BEGIN):])
+
+
+def _extract_attached_headers(msg):
+
+ def split_header(line):
+ sep = ': '
+ split = line.split(sep)
+ return (split[0], sep.join(split[1:]))
+
+ first = msg.get_payload()[0]
+ if first.get_content_type() == 'text/rfc822-headers':
+ raw = first.get_payload()
+ headers = dict([
+ split_header(line) for line in filter(None, raw.split('\n'))])
+ return headers
+
+
+def _build_embedded_header(msg):
+ r = ''
+ for x in COPY_HEADERS:
+ if msg.get(x):
+ r += x + ': ' + msg.get(x) + '\n'
+ return r
+
+
+def _boundary_factory(start):
+ counter = {'value': ord(start)}
+
+ def _gen_boundary():
+ boundary = chr(counter['value']) * 12
+ counter['value'] -= 1
+ return boundary
+ return _gen_boundary
+
+
+
+def _wrap_with_header(msg, boundary=None):
+ body = email.message.Message()
+ body.set_payload(msg.get_payload())
+ body.add_header('Content-Type', msg['Content-Type'])
+
+ emh = email.message.Message()
+ emh.set_type('text/rfc822-headers')
+ emh.add_header('Content-Disposition', 'attachment')
+ emh.set_payload(_build_embedded_header(msg))
+ del emh['MIME-Version']
+
+ wrapper = email.message.Message()
+ wrapper.set_type('multipart/mixed')
+ if boundary:
+ wrapper.set_boundary(boundary())
+
+ wrapper.set_payload([emh, body])
+ del wrapper['MIME-Version']
+ return wrapper
+
+
+def _make_signature_subpart(signature):
+ message = Message()
+ message['Content-Type'] = 'application/pgp-signature; name="signature.asc"'
+ message.set_payload(str(signature))
+ return message
+
+if __name__ == "__main__":
+ import os
+ import sys
+ import logging
+ if os.environ.get('DEBUG'):
+ logging.basicConfig(level=logging.DEBUG)
+ if len(sys.argv) != 3:
+ print "Usage: memoryhole mail_path key_path"
+ sys.exit()
+ msg_path = sys.argv[1]
+ key_path = sys.argv[2]
+ parser = Parser()
+ gpg = gnupg.GPG(
+ binary='/usr/bin/gpg',
+ homedir='/tmp/memoryhole-tests/', use_agent=False)
+
+ with open('tests/corpus/OpenPGP/' + key_path + '.key') as key_f:
+ key_data = key_f.read()
+ gpg.import_keys(key_data)
+ with open('tests/corpus/OpenPGP/' + key_path + '.pgp') as key_f:
+ key_data = key_f.read()
+ gpg.import_keys(key_data)
+
+ #if not gpg.list_keys():
+ #key_input = gpg.gen_key_input(key_length=1024, key_type='RSA')
+ #gpg.gen_key(key_input)
+
+ with open(msg_path) as f:
+ basetext = f.read()
+ basemsg = parser.parsestr(basetext)
+
+ boundary = _boundary_factory('c')
+ msg = protect_message(
+ basemsg, gpg, boundary=boundary,
+ sign_digest_algo='SHA256', passphrase='_' + key_path + '_')
+ print msg.as_string()
diff --git a/requirements.pip b/requirements.pip
new file mode 100644
index 0000000..e3b45e5
--- /dev/null
+++ b/requirements.pip
@@ -0,0 +1,2 @@
+# use peep!
+gnupg
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/setup.py
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/corpus/OpenPGP/.gitignore b/tests/corpus/OpenPGP/.gitignore
new file mode 100644
index 0000000..03f357b
--- /dev/null
+++ b/tests/corpus/OpenPGP/.gitignore
@@ -0,0 +1 @@
+GNUPGHOME
diff --git a/tests/corpus/OpenPGP/README.md b/tests/corpus/OpenPGP/README.md
new file mode 100644
index 0000000..037d893
--- /dev/null
+++ b/tests/corpus/OpenPGP/README.md
@@ -0,0 +1,4 @@
+This directory contains OpenPGP public and private key material that
+can be used to interpret messages from this corpus.
+
+See the genkey script for how they were generated.
diff --git a/tests/corpus/OpenPGP/genkeys b/tests/corpus/OpenPGP/genkeys
new file mode 100755
index 0000000..d5e74b8
--- /dev/null
+++ b/tests/corpus/OpenPGP/genkeys
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# Daniel Kahn Gillmor <dkg@fifthhorseman.net>
+# 2015-07-15
+
+# How to generate the keys used for the creation of the message
+# corpus.
+
+export WORKDIR=$(mktemp -d)
+export GNUPGHOME="$WORKDIR"
+
+cat >"${WORKDIR}/gpg-agent.conf"<<EOF
+allow-loopback-pinentry
+no-allow-external-cache
+EOF
+cleanup() {
+ gpgconf --kill gpg-agent
+ rm -rf "$WORKDIR"
+}
+
+trap cleanup EXIT
+
+gpg2 --batch --pinentry-mode=loopback --passphrase=_winston_ --quick-gen-key 'Winston <winston@example.net>'
+gpg2 --batch --pinentry-mode=loopback --passphrase=_julia_ --quick-gen-key 'Julia <julia@example.org>'
+gpg2 --batch --pinentry-mode=loopback --passphrase=_obrian_ --quick-gen-key "O'Brian <obrian@example.com>"
+
+for x in winston julia obrian; do
+ gpg2 --armor --batch --pinentry-mode=loopback --passphrase=_${x}_ --output $x.key --export-secret-key $x
+ gpg2 --armor --batch --output $x.pgp --export $x
+done
+
diff --git a/tests/corpus/OpenPGP/gnupg-import b/tests/corpus/OpenPGP/gnupg-import
new file mode 100755
index 0000000..a92bce2
--- /dev/null
+++ b/tests/corpus/OpenPGP/gnupg-import
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+mkdir -p -m 0700 GNUPGHOME
+export GNUPGHOME=$(pwd)/GNUPGHOME
+
+cat >GNUPGHOME/gpg-agent.conf <<EOF
+allow-loopback-pinentry
+no-allow-external-cache
+EOF
+
+for x in winston obrian julia; do
+ <$x.key gpg2 --batch --pinentry-mode=loopback --passphrase=_${x}_ --import
+done
+
+# mark all keys with ultimate ownertrust. This is a silly hack, but
+# it's due to the fact that this corpus hasn't really decided whether
+# the GnuPG homedir is supposed to represent one particular user's
+# config (in which case we should just import one secret key, mark it
+# with ultimate ownertrust and have it sign the others) or just
+# pragmatically handle all the secret keys in a single homedir. For
+# now, we're opting for the latter.
+
+gpg2 --import-ownertrust <<EOF
+5A7AD43844FB30BE7DB1B3FD15FB4EBC8E2D6CB7:6
+9A9CC2E1546C7A04D23048641BC98889B8EA08B3:6
+2BC85B8EF240B7422E9C19F2E0D7563140A09310:6
+EOF
diff --git a/tests/corpus/OpenPGP/julia.key b/tests/corpus/OpenPGP/julia.key
new file mode 100644
index 0000000..e2c558e
--- /dev/null
+++ b/tests/corpus/OpenPGP/julia.key
@@ -0,0 +1,59 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v2
+
+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
+aDjtAVjGPvw5WbpSXHH3YM7eK3LJPKsAvQ==
+=sfSJ
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/tests/corpus/OpenPGP/julia.pgp b/tests/corpus/OpenPGP/julia.pgp
new file mode 100644
index 0000000..c3bf3c2
--- /dev/null
+++ b/tests/corpus/OpenPGP/julia.pgp
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v2
+
+mQENBFWmZK8BCAC+iejpS2pWGQT3I9diXAMO5j+MvSRRJCJ8r/aFwKFoSO3FwDEn
+hKT7T5Fj1S/NEahAJgjw3pghNpA/C/CVSy9+lCUDjpRDyYX+18bXrc5DsGD3OHNt
+95QLyyOsed/nGbVvpPvwl/TMMjzEw+NVtjb84F2n52YfcGhXoEzweWozSuBvyjBZ
+/8vWREytW/J3SBQTGfC/dPfoN8PrA1SkpCkE7bOAVEibcWK5gs7BHsxNxLXLF8EX
+Yy0F+XIJIC0bRCJk7akJRhu+dCsVFOOldSKg+RVl65ntSxTx8f/iJGAAAOqu5Ozj
+B3tM0Yd88msUcPmW/hLaJmT888v89+Pj7YplABEBAAG0GUp1bGlhIDxqdWxpYUBl
+eGFtcGxlLm9yZz6JATcEEwEIACEFAlWmZK8CGwMFCwkIBwIGFQgJCgsCBBYCAwEC
+HgECF4AACgkQ4NdWMUCgkxD43ggAnPFcGXv6PZ91U6l++KSddBg2TyQ3ZVrys/pH
+4mkgw7KV17GE9n0cd6t1O3N9uatZoKX7K2O998lcMLo8NX/XBZxW83b3XDwunaYO
+ncQf29W2IeUJYIR9ORYb13sY5ddlER6UVB9loeHcMwPQMRjZzZnzO10+9tB3ZxNR
+p2ujEAsDI8lzC29i79ki1JsS1TD9RpEsvkFL8IQjo0pILoDYlYlBF1hEd5+cm46r
+EZdC5pRmWMaZCAyLrbbmVJF1FkvHfqWhiHv7ZRwhWgBygBVXzvq01QR2pPf2VTHI
+EE1wYBj5YkrdMOfv2iyM4g0x4P1i6oEf34bOgq0Boy68K9uFLrkBDQRVpmSvAQgA
+pYNrQfTiZvnCdvAahE0AF9QBwhTNHVxcIiT8Vxeyq3vTHPugXD3KKyBmAhfY3Ja3
+EJMgPMBoa08AMYKNo2BOCXqdKsnGWNhEobJw1SRJL9rGcCnt/WU6ZS9CR1/Uhe5T
+FI/msQzHBVtcJ0aI/Eglx95yyrHBqjw2WRK9HkDiu8nHWKGsT5uZg8vnn25SYSv7
+MPTp2vatY2m4Ncbb98/AReU2+uYIWhCLmeEASgq9SKL2z+laPnJ5vTI2c9beVhJc
+PTyIi3Jm+cXJ3Gg92bX2VMV2v5G+W6MfZV731RqWA3EDqJnt3mfVUp2sSirskcSr
+YRn3jnhcUE8fHaaRLVlfYwARAQABiQEfBBgBCAAJBQJVpmSvAhsMAAoJEODXVjFA
+oJMQYlUH/ReACSde3FPpSUBlpR4s8Yv5j3aDFl1L2usf1znAxs1Xm1t8Z17/dYxX
+2U1tHjw0QeF0QpeQS+rbVkz4HZm1pfbrZKerO465zvR24gmKjFFLz00d1vfpzEwg
+h0Gc7KKdWf38jnjICde58gqMEsl0Kt6OwfLsYPImh8gNtvcLqTVg72tYvcwZ/Wd4
+bD/xuWk0SSpTye2bgRy6k+g/h8Pv4pE72xrA5VsOnSojxzm9joCo53+Aht+BSDO/
+HYsv5mCu4UtXlOu/e9p2RDgjSbIaDw8eoFCo/jZeDXz4fXrgZVlZBN2B0ExQrWg4
+7QFYxj78OVm6Ulxx92DO3ityyTyrAL0=
+=8jVN
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/tests/corpus/OpenPGP/obrian.key b/tests/corpus/OpenPGP/obrian.key
new file mode 100644
index 0000000..37bd877
--- /dev/null
+++ b/tests/corpus/OpenPGP/obrian.key
@@ -0,0 +1,59 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v2
+
+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
+MuCm/QNgbu+E4rf3zmvTHhjAXyxT6kfXCwxdbA==
+=WTVE
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/tests/corpus/OpenPGP/obrian.pgp b/tests/corpus/OpenPGP/obrian.pgp
new file mode 100644
index 0000000..0751fc6
--- /dev/null
+++ b/tests/corpus/OpenPGP/obrian.pgp
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v2
+
+mQENBFWmZLABCADTWhsDvyR2bWT/edmFt5vOiBVGT3lR5BnxdP8j6rlWLZ+T+FZZ
+JeUzuV87zaGvjlj2Sl11X61qh963WxyUJGg1GjIRg1mT1n9o0FPmTAet2iQr2PFv
+GSdwFgPg9XvK+O99ij3+oAGTkjXtXuy+y/rrD9k4E1k63q0uHBpiTJ428mab9+Jo
+cQEcwzPwfU7yTtYb1oQChuNto0eeQajeNc9oFcklZ1FLDKfedPiYtXJuSzo1PeDz
+ViYDX1qY/CmTKZjhIjYxBEsa5DRdHsRNqMDojxi4eUORIGBAcDF1Jrz6wWrFa3Vs
+K7J88aGX3y4BMA9HL+aMyl4mauwLm5YByBNpABEBAAG0HE8nQnJpYW4gPG9icmlh
+bkBleGFtcGxlLmNvbT6JATcEEwEIACEFAlWmZLACGwMFCwkIBwIGFQgJCgsCBBYC
+AwECHgECF4AACgkQG8mIibjqCLMX0wgAhPWPL7fRW4ZoLhSYeBMtiyZGZ3TR1TXG
+UOa8OZw387cRPOOkdf7SbhfjqEVfyOyH56L10y+uu5GwoU0E+iNDH5qy63YJRisd
+27IiK9TPwYVn7E1eOHgtnSiCniUKjfa0EJV+8gZMmCQdjE1lwPPLmwKxqWN21kiS
+JcWcYz3E7ctDiHzlmmpWna9BQAAbqJYnbMWF9YmtdQ4jlkRI8o6PiSh5IZ+dn81b
+5GNqXm8WYhvIQrYiCmQRsc4fC+B/mfMjFQiYjN/jLuWmJg1WJ/w3k6KNy8ocmMQV
+KOFGbGEtCrw2wUmar7qlEVj7bvmH7+uCE6VRDAJ4SmTgAzQP8f+bObkBDQRVpmSw
+AQgAn1e3f3xHlPUXvC4LQzCu5YnWDK13DVhobuHz5rymQJX8gWhvp8HUOYUtAGXc
+4lhctJzVViZLUsLl8ixcgUCC78lZnAcNE1jQ65MB4wBCSD3V2Jmw7aPLFZtO0lqL
+z40Y1FhOgcQd+AdUEqVCu1T/Hbu/+Pjkh4ZE9DXkogsgD1OCRL1QaegvnMjxACyS
+z8bw+8rCg1stGbTQ/hydNajCySpc33Sb/6Bt0rw+tY8wZVCwfT6h1nEmTAE3UG00
+KjFOY+Xy69MM7QMYZ3+m/Ujad51vG28GXvGPy8hUzfQJn4ih8+q8p9GfCX7TYZe7
+Rv+rNG7SVcBs+velGPou4QIUHwARAQABiQEfBBgBCAAJBQJVpmSwAhsMAAoJEBvJ
+iIm46gizncYIAITjPmeFbNl+odoYEqtR46o1wxmlElYDWNohgNzLiPOK1QHGnDOF
+hLwBMtInbcP7uU0AIg1ov+wwZiMKQz+cebBA5CeUu/pQHA1ftkjX/myNJ5kTxVvw
+dh6LtH4wQO9nAm8/o1+WX0qZuWAGzZgiRZTeZlQHqjjbrA/TKt5+36ZQfOznCr5+
+DznMNcvS3oD/FPJd7conhBHSmhHJ2Fx4+c9lp+GbB7N4BxCbsIgl4g2yZwdFa6i6
+yxy/smHxDyz3lZvdN3Ls0BjFifI/uQMWdKjBI3W5iUlhRqOr5oWVWfsnevYcuDLg
+pv0DYG7vhOK3985r0x4YwF8sU+pH1wsMXWw=
+=05aR
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/tests/corpus/OpenPGP/winston.key b/tests/corpus/OpenPGP/winston.key
new file mode 100644
index 0000000..8a29764
--- /dev/null
+++ b/tests/corpus/OpenPGP/winston.key
@@ -0,0 +1,59 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v2
+
+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
+uWPeDAsLSS+VXRtnvjQBPQk2D3N+eCqBX+XMog==
+=qHx1
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/tests/corpus/OpenPGP/winston.pgp b/tests/corpus/OpenPGP/winston.pgp
new file mode 100644
index 0000000..b008d61
--- /dev/null
+++ b/tests/corpus/OpenPGP/winston.pgp
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v2
+
+mQENBFWmZKwBCACc1JThxcPtPcS9K33SnyzKsDjqo1G5pL3hyxlB/Y/lHBESIcoH
+KtTm8H/tSxHCpBWn+NK84cFDw1bw+lCRx8xjocdBBg7R0IUcUePBu7GD602mNRSn
+lTbvOaeaJxjeO9pckd/Ag2Np3hRktXmVZqISeiqeFGvqlTNgLnSngHzRl4hfmdyb
++cqMT3w3rSCH4/Nb0o+qEm7OeRk2NmqQnkDCPBGPwDLZDLGyMPruo4DbNjylJ4+e
+2/Oaq/iIK1VXU5CzFarC5UHTOMCK9ymsnL3QSJj8fmTjptZlEvmqs0FN396unuEO
+hQrmokJJl6Ro9PBifsvhllkQIhCwdgXlujIzABEBAAG0HVdpbnN0b24gPHdpbnN0
+b25AZXhhbXBsZS5uZXQ+iQE3BBMBCAAhBQJVpmSsAhsDBQsJCAcCBhUICQoLAgQW
+AgMBAh4BAheAAAoJEBX7TryOLWy3rYQH/j4cv+OTjrJeQepupq2tTgtMepdWmr7v
+RW/1VJw6PKk77WLWvnBmLnpDKS/ageEkv5pjsTSB9RqoUmTj6n5MZArIVid1B3q6
+qQ9hwdC1kIGuTYZd+/nEulsxu+r8xTOwcnfKCTVPuP4X8h29dFwEOg8WQ0WNgiFG
+o6Sgogtx3rNApGMWRItUkNGQmKvHzG5XiGkRwfBWPKsUwN91Onnx4kdD1lzWvwRl
+bWkyBKgrD79tZeRyB87Ns6LZ+C3MOZ3ucfL9/UbKf1Mvzsg8hc9//v2i/zhP9ArY
+XefGtUu9N03NvGWQYbLIDb7ZihLpswCGanxUUbHQMOYz/g1uI862Q2W5AQ0EVaZk
+rAEIAMEzK5hvFn7lxQtkVZ1xoJ0KIb8UwgpRMAqRTzlfjn72NmdVDaHfuWD5Xn1Q
+cQcV1/yypkwfkHbo1jbJFaChZmAFaQ2inutqzakDLRMpJJc/uILaIiojskoILZtp
+wGNlEvtvZrJzuduV8+TH/NKRXP8mG24lIfXQFGRRs+/XNjoyjyINPnsHG9EGHJlB
+oZx9tiMlLJQwC1fJITUoM5m7OYEv7bpTYc2FmIXyAwk8tNKiRfd3M2BWAnwfCF/V
+maBewmMLHU5YRfRCD1x3R28RQcwyBUSaN5+va0YY0WdBHgTt4zJd/hnT+N001rsp
++VfVDT10GDM4DcNNEPv58xLmTKcAEQEAAYkBHwQYAQgACQUCVaZkrAIbDAAKCRAV
++068ji1stxKzB/4iRi/oQ43mvZoG0IODJSqa8nOQJtaaKG3ekHg07kaC/FAjPeTg
+bxiqcMiKKGOkgQYHhxnZBob8ykr4fwuxpskUJQ2iiEp4+lhBU/Y5zbktZA9761mC
+hUM6EFPGvcmeyG+cRBPcc+GJL1yKLXZwhN1gY/BTeieKgAzDFpsChm+MPaPoJ98e
+nLQpy/f3yi4evayWRfnSQR9EMkt5C/1umhzutOlzjSaigyd0eT3j2SD5LGkEe8GX
+Iwj1QtsI8Cj88olUbpcZfVIAx5Ee7Hin/jtvjac9uvmv6zJ/YNg+cc29EsYYYLi5
+Y94MCwtJL5VdG2e+NAE9CTYPc354KoFf5cyi
+=Wk6a
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/tests/corpus/expected.A.eml b/tests/corpus/expected.A.eml
new file mode 100644
index 0000000..437f3db
--- /dev/null
+++ b/tests/corpus/expected.A.eml
@@ -0,0 +1,69 @@
+Content-Type: multipart/signed; protocol="application/pgp-signature";
+ micalg="pgp-sha256"; boundary="cccccccccccc"
+MIME-Version: 1.0
+Date: Thu, 16 Jul 2015 11:44:44 +0200
+Subject: alternative text/html message with embedded header, signed
+Message-ID: A@memoryhole.example
+To: Julia <julia@example.org>
+From: Winston <winston@example.net>
+
+--cccccccccccc
+Content-Type: multipart/mixed; boundary="bbbbbbbbbbbb"
+
+--bbbbbbbbbbbb
+Content-Type: text/rfc822-headers
+Content-Disposition: attachment
+
+Date: Thu, 16 Jul 2015 11:44:44 +0200
+Subject: alternative text/html message with embedded header, signed
+Message-ID: A@memoryhole.example
+To: Julia <julia@example.org>
+From: Winston <winston@example.net>
+
+--bbbbbbbbbbbb
+Content-Type: multipart/alternative; boundary="aaaaaaaaaaaa"
+
+--aaaaaaaaaaaa
+Content-Type: text/plain
+
+This is a test
+message on multiple lines
+
+with a silly bit more.
+--
+and a .sig here.
+
+--aaaaaaaaaaaa
+Content-Type: text/html
+
+<html>
+<head>
+<title>titles are usually unrendered</title>
+</head>
+<body>
+<p>This is a test<br/>message on multiple lines</p>
+<p>with a silly bit more.</p>
+<hr/>
+<p>and a .sig here.</p>
+</body>
+</html>
+--aaaaaaaaaaaa--
+
+--bbbbbbbbbbbb--
+
+--cccccccccccc
+Content-Type: application/pgp-signature; name="signature.asc"
+
+-----BEGIN PGP SIGNATURE-----
+
+iQEcBAABCAAGBQJWAiCsAAoJEBX7TryOLWy3JpEH/Ao8INvddDi7OJhsXWlLAHiu
+GaDS/xSXvsXgVe3IstKQLeepwvjkrismFNxQ7+5uBJxVXr+TgQgG8KAXZM7VYl+D
+rBnZYTlLnepSeeqeS3gcB9EWNABopCuDjEdCDO+CHudR4vFaN8wlqafxMdKqR74z
+i+49RXUXlBJMJJNNKrzJyatltRRyGBM0sqUIt6ZqrhwT5DwJZFbMBLx23X9heFFi
+7oLGSrAzlSfVr8KM5DoniAyNXSNWtVjj1hl8xCVJtIfsOcvpwCVR15vzfuH623og
+0Ag7G0GyKb1yNKlIZeUvJ7uo4Tm2+gUikn7Pk1eGqAEC5R7Ip1u8bkEtgdftinw=
+=OeWg
+-----END PGP SIGNATURE-----
+
+--cccccccccccc--
+
diff --git a/tests/corpus/sample.A.eml b/tests/corpus/sample.A.eml
new file mode 100644
index 0000000..cf67839
--- /dev/null
+++ b/tests/corpus/sample.A.eml
@@ -0,0 +1,32 @@
+Subject: alternative text/html message with embedded header, signed
+Message-ID: A@memoryhole.example
+Date: Thu, 16 Jul 2015 11:44:44 +0200
+To: Julia <julia@example.org>
+From: Winston <winston@example.net>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="aaaaaaaaaaaa"
+
+--aaaaaaaaaaaa
+Content-Type: text/plain
+
+This is a test
+message on multiple lines
+
+with a silly bit more.
+--
+and a .sig here.
+
+--aaaaaaaaaaaa
+Content-Type: text/html
+
+<html>
+<head>
+<title>titles are usually unrendered</title>
+</head>
+<body>
+<p>This is a test<br/>message on multiple lines</p>
+<p>with a silly bit more.</p>
+<hr/>
+<p>and a .sig here.</p>
+</body>
+</html>