diff options
authorDaniel Kahn Gillmor <>2019-11-25 16:45:49 -0500
committerSean Whitton <>2019-11-28 11:09:01 -0700
commit15ed2184e61e40a35e56921aa57a49726f56b5c2 (patch)
parent3c84e68d79ed84f916f1b983168d58e0f360686b (diff)
email-print-mime-structure: decrypt S/MIME parts with OpenSSL
If the user supplies a secret key like the ones found in, then email-print-mime-structure will try to use that for decryption of CMS-encrypted (S/MIME) message parts. Signed-off-by: Daniel Kahn Gillmor <> Acked-by: Sean Whitton <>
4 files changed, 32 insertions, 5 deletions
diff --git a/debian/control b/debian/control
index f04ce79..d2e07da 100644
--- a/debian/control
+++ b/debian/control
@@ -12,6 +12,7 @@ Build-Depends:
gpg-agent <!nocheck>,
gpgsm <!nocheck>,
mypy <!nocheck>,
+ openssl <!nocheck>,
python3 <!nocheck>,
@@ -54,6 +55,7 @@ Suggests:
+ openssl,
Architecture: all
Description: collection of scripts for manipulating e-mail on Debian
This package provides a collection of scripts for manipulating e-mail
diff --git a/email-print-mime-structure b/email-print-mime-structure
index e82d56e..4de0789 100755
--- a/email-print-mime-structure
+++ b/email-print-mime-structure
@@ -83,7 +83,7 @@ class MimePrinter(object):
print(f'{prefix}{z.get_content_type()}{cset}{disposition}{fname} {nbytes:d} bytes')
cryptopayload:Optional[Message] = None
try_pgp_decrypt:bool = self.args.pgpkey or self.args.use_gpg_agent
- try_cms_decrypt:bool = self.args.use_gpg_agent
+ try_cms_decrypt:bool = self.args.cmskey or self.args.use_gpg_agent
if try_pgp_decrypt and \
(parent is not None) and \
@@ -116,6 +116,12 @@ class MimePrinter(object):
if cryptopayload is None and self.args.use_gpg_agent:
cryptopayload = self.pipe_decrypt(ciphertext, ['gpg', '--batch', '--decrypt'])
elif flavor == EncType.SMIME:
+ if self.args.cmskey:
+ for keyname in self.args.cmskey:
+ cmd = ['openssl', 'smime', '-decrypt', '-inform', 'DER', '-inkey', keyname]
+ cryptopayload = self.pipe_decrypt(ciphertext, cmd)
+ if cryptopayload:
+ return cryptopayload
if self.args.use_gpg_agent:
cryptopayload = self.pipe_decrypt(ciphertext, ['gpgsm', '--batch', '--decrypt'])
if cryptopayload is None:
@@ -175,7 +181,9 @@ def main() -> None:
parser:ArgumentParser = ArgumentParser(description='Read RFC2822 MIME message from stdin and emit a tree diagram to stdout.',
epilog="Example: email-print-mime-structure <message.eml")
parser.add_argument('--pgpkey', metavar='KEYFILE', action='append',
- help='OpenPGP Transferable Secret Key for decrypting')
+ help='OpenPGP Transferable Secret Key for decrypting PGP/MIME')
+ parser.add_argument('--cmskey', metavar='KEYFILE', action='append',
+ help='X.509 Private Key for decrypting S/MIME')
parser.add_argument('--use-gpg-agent', action='store_true',
help='Ask local GnuPG installation for decryption')
parser.add_argument('--no-use-gpg-agent', action='store_false',
diff --git a/email-print-mime-structure.1.pod b/email-print-mime-structure.1.pod
index f109997..037c1a9 100644
--- a/email-print-mime-structure.1.pod
+++ b/email-print-mime-structure.1.pod
@@ -32,15 +32,26 @@ key.
OpenPGP secret keys listed in B<--pgpkey=> are used ephemerally, and
do not interact with any local GnuPG keyring.
+=item B<--cmskey=>I<KEYFILE>
+I<KEYFILE> should name a PEM- or DER-encoded X.509 private key that is
+not password-protected. If an S/MIME-encrypted message that uses CMS
+is found on standard input, this key will be tried for decryption.
+May be used multiple times if you want to try decrypting with more
+than one such key.
+X.509 private keys listed in B<--cmskey=> are used ephemerally, and do
+not interact with any local GnuPG keyring.
=item B<--use-gpg-agent>
If this flag is present, and B<email-print-mime-structure> encounters
a PGP/MIME- or S/MIME-encrypted part, it will try to decrypt the part
using the secret keys found in the local installation of GnuPG.
-If both B<--pgpkey=>I<KEYFILE> and B<--use-gpg-agent> are
-supplied, I<KEYFILE> arguments will be tried before falling back to
+If B<--use-gpg-agent> is supplied along with either
+B<--pgpkey=>I<KEYFILE> or B<--cmskey=>I<KEYFILE> arguments, the
+I<KEYFILE> arguments will be tried before falling back to GnuPG.
If B<email-print-mime-structure> has been asked to decrypt parts with
either B<--pgpkey=>I<KEYFILE> or with B<--use-gpg-agent>, and it
diff --git a/tests/ b/tests/
index 6e510ca..b45294e 100755
--- a/tests/
+++ b/tests/
@@ -22,6 +22,12 @@ for eml in tests/email-print-mime-structure/*.eml; do
GNUPGHOME="$testgpghome" test_eml "$base" --use-gpg-agent
rm -rf "$testgpghome"
elif [ -e "$p12key" ]; then
+ printf "Testing %s (OpenSSL)\n" "${eml##*/}"
+ grep -v ^- < "$p12key" | base64 -d | \
+ openssl pkcs12 -nocerts -nodes -passin pass: -passout pass: -out "$base.pemkey"
+ test_eml "$base" --cmskey "$base.pemkey"
+ rm -f "$base.pemkey"
testgpghome=$(mktemp -d)
printf "Testing %s (GnuPG S/MIME)\n" "${eml##*/}"
gpgsm --pinentry-mode=loopback --passphrase-fd 4 4<<<'' --homedir="$testgpghome" --batch --quiet --import <"$p12key"