๐Ÿ“† March 15, 2023 | โฑ๏ธ 5 minutes 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))
    else:
        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(' ', '')
        print(decode_dkim_private(txt).exportKey().decode())

Here’s an example run retrieving my old key:

$ ./dkim-private.py 'nicholasjohnson.ch' 'mail'
-----BEGIN RSA PRIVATE KEY-----
MIIEoAIBAAKCAQEAu1rjJVpWjzgrBBYaWhgRdwGbjW7utaIDXf/te4MjWO4XNskM
DfTUBrI3O2LZYQNdvwP4zu8O+VtT5P7az1yNBmuseQtTb+/qGdvxEZhb+zSrKgKC
Vrvac4f09JFOsTvc4VAH537QCRh7whpc625R93NVsGFAuQhIOVpbOWLRbUB+Deys
CbiEyQm9H1PYa+x/M2cMHkVw8xVgmeET7q2jSiy9E3QyIwmWVl5qKqeXYbHPKotU
NTSRfSC5au53UEr7PRrA60osLSqav/hVme0QvydpxLqaoYQz0ntQ4VgNIeReEVjl
UaDUjbCNjzrZYgAp4e9lNDAXBJAYZuT8kyxorQIDAQABAoH/NLFbMSY3MhCaCIwu
3SfnwZCyLxUEDhGC4O0Z3aMETf6oiMACo+o3t6pn3kvu11obA54aXBjgHUGSVtVW
tPtSrnuaBnEpBlJzhNJW+pvfQseNXENYZQxwZA3Y1vAHMdGkTbo0fucrm2NHa0/+
0jg01WOatgCkM2Yg6gB/p6QjQpZkraPGOlW5P5Yc5HWDJFvTnpVMOGrN7iPAxNVT
+SvBCJR0Z0mqUdNTE9dcjt2ZfWdpYNJbjBdRtUMs4PHICy+octx/qVrP53bbk1XX
XSfbXsMrghpJAg+Tsk1jsA3IdrkOD06abqUxEouzOe6qLY2T3Ws1kuEnrnB4h+WX
ogrhAoGBAPiyrYb/JF7Kh2jPz0nTR2CjEWTyvhDe87NkX8KpbhPixcMuzR9S2lIN
r3A42J2vaZzh3/ZqQGY+eqYNDFoUIguXDhXfDsBV1M6oPblSepvRbmj2kWVuaGC5
QlhgChRlrwFcVSxE2uyX7K0QD6qH363V6xcOLgAK/w9lDlVRLxynAoGBAMDbIRdK
MMProQt/Qx48qMlO4J0zp0mCjXgza6O8XPlX/iEQPCmK4k9Lpk3bqtv876hkSMAJ
W8FKzZn55uDVKf8rUGkwTxIIIU+BtZKukavyob/X27hci4W9NRwYnhNW2qEJD5Zr
47ATJK+Hljyur5NzJAqelNT7AkWOwOHRgpaLAoGAO3AAtwXdfGYtDKi14vAC3B68
9oJpWIDgf5xaopx5uXj2SNqznWvgz8GDj2+WncEhnaQCMdNdBtKh33O44wJyzJBS
hnmj/eXFkYp2DgefVAQuvhlH0JUdjxhXueViI1PWCp41oPnn1KnOn+H5zIjitZot
sHnsFoiKQvSlA1D+0HsCgYAxiNcSBVNLLz1ZF6HkpU3xDtpwZjEEl4zAn+x4zMvQ
m4JBecsKHIsONO8NNmvHP0tLJB7vfDfeCNmQP/jGLNoxmS15JxhYGFB9/GHnwADY
emSDQu1DiDmp6zQ1+Di53OggzpP7XdDIi9IzZ472HSQpqjxKofq2TZaCySzPk6GI
CwKBgG8D4LB0WryU+Jg0aV9+jBHheAFXs7UfvwShfh6F15IVp61AIROnyLRYByLg
7XAtm7jpuIPtce2etnCtjiM3Tbp0qallLAV2XlWXqwMXhy85ox6qlkG9VmrjHwyb
SX+8PTuQN1wQH9qocI1gXOEzU0mV74Joywc6t6UL+4xIwsmQ
-----END RSA PRIVATE KEY-----

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.