📆 March 15, 2023 | ⏱️ 5 minute read | 🏷️ computing

Re: DKIM: Show Your Privates

I recently read Ryan Castellucci’s blog post, “DKIM: Show Your Privates”. The problem Ryan points out is that DKIM, which signs outgoing emails as a way to to reduce spam, has a negative unintended consequence: it’s harder to deny that you sent an email if it gets leaked. As Ryan points out, saner messaging protocols like OTR and the Double Ratchet Algorithm do implement cryptographic deniability of messages.

There is a way to mitigate the loss of cryptographic deniability in email. You simply rotate DKIM keys, invalidating the old one and publishing its private part. The point of publishing the private part is that any leaked emails which were signed with that key could be forged. Thus, one can deny past emails signed with that key.

As Ryan notes in their blog post though, email deniability probably won’t protect you from the law:

“I alluded to it earlier, but I want to be clear — publishing DKIM private keys like this mainly addresses leaks as a threat model. In a legal dispute, mail server logs and/or stored mail can be subpoenaed if the authenticity of messages is in question. Even in my case, where I have my own mail server on dedicated hardware with full disk encryption at an undisclosed location, most mail I send will be delivered to a server operated by a third party with no incentive to alter logs at the behest of the recipient.”

The Session team’s blog post, “Session Protocol: Technical implementation details”, says more or less the same in the context of their own private messaging protocol:

“As previously mentioned, cryptographic deniability is often something that is largely ignored by the court system and the media. If contextual information can be provided around screenshots, this is often enough to lead to a conviction or personal damages, regardless of the presence or absence of cryptographic deniability.

Instead of designing a cryptographic protection, Session will add the ability to edit other users’ messages locally, thus providing a way to completely forge conversations. Since signatures are deleted after messages are received, there will be no way to prove whether a screenshot of a conversation is real or edited, diminishing the value of screenshots as evidence.”

Programmers could still change the Session source code to save the message signatures anyways, but I highly doubt anyone is doing this. By contrast, email servers do retain email signatures even after emails are already validated. So there’s more of a concern for email being cryptographically undeniable than Session Private Messenger.

So, in my opinion, all email providers should publish expired DKIM keys. Especially the big ones that handle lots of mail like AOL, Gmail, iCloud, Outlook, Yahoo, Yandex, etc. I’ll quickly debunk some of the main objections.

Publishing expired keys doesn’t make spam harder to combat since the key gets revoked anyways. Since email accounts rely on the security of both client and the server and many people use weak passwords and fall victim to phishing attacks, determining whether someone sent a particular outgoing email depends more on context (server logs, IP addresses, other info) than DKIM signatures. So publishing expired keys probably doesn’t make computer forensics harder.

This practice could make valuable corporate/government email leaks less credible, but CEOs and politicians aren’t the only ones who use email. Everybody uses it and making everybody less safe to gain slight confidence that occasional, albeit important email leaks are legitimate doesn’t seem worth it.

Practicing what I preach, I’ve published my old DKIM keys in the DNS system using some of Ryan’s code. Since my DKIM key uses more bits, I tweaked their dkim-private.py to support recovery of 2048+ bit RSA signing keys:

#!/usr/bin/env python3
import gmpy2, sys, dns.resolver
from Crypto.PublicKey import RSA
from base64 import b64decode as b64d

def decode_dkim_private(txt):
    params = dict()
    # Parse the DKIM selector record.
    for key, _, val in map(lambda x: x.partition('='), txt.split(';')):
        if key == 'n':
            for k, v in map(lambda x: x.split(':'), val.split(',')):
                params[k] = int.from_bytes(b64d(v), 'big')
    # Compute rest of RSA keypair parameters (if possible).
    if all (k in params for k in ('e', 'p', 'q')):
        params['n'] = params['p'] * params['q']
        phi = (params['p'] - 1) * (params['q'] - 1)
        params['d'] = int(gmpy2.invert(params['e'], phi))
        rsa = map(lambda x: params[x], 'nedpq')
        return RSA.construct(tuple(rsa))
        return None

if __name__ == '__main__' and len(sys.argv) == 3:
    domain = sys.argv[1]
    selector = sys.argv[2]
    for answer in dns.resolver.resolve(selector + '._domainkey.' + domain, 'TXT'):
        txt = str(answer).replace('"', '').replace(' ', '')

Here’s an example run retrieving my old key:

$ ./dkim-private.py 'nicholasjohnson.ch' 'mail'

I’ll continue to rotate my keys on a regular basis. If you have the skill to administer your own email server, then you’re probably capable of figuring out how to rotate DKIM keys. If you need some inspiration, Ryan wrote an experimental Python script which fully automates the procedure.

I know it sounds like a lot of effort, and it’s absurd to me too that email in its present incarnation is such a disaster of a protocol that it requires doing this sort of thing. This is yet another reason we desperately need to replace the existing network stack.