If you’re like most people, there’s a good chance that it’s been years since you’ve sent an email that wasn’t cryptographically signed. You don’t use PGP, you say? Well, even if you are not signing your email, your provider is almost certainly doing it for you. Plausible deniability has been tossed aside in the name of stopping spam, but it doesn’t have to be.

DKIM, originally standardized in 2007 by RFC 4871, now has near universal[1] adoption. To quote the RFC, the goal behind the protocol is to “permit a signing domain to assert responsibility for a message, thus protecting message signer identity and the integrity of the messages they convey”. It’s one of several technologies used prevent the sender identity information in email from being spoofed[2]. Anti-spam systems use it to help determine whether to consider the reputation of a domain name when making a processing decision.

While DKIM was designed to be useful for spam prevention, the cryptographic signatures it uses have quietly made a property called “non-repudiation” the new normal for email. The term is used in in contract law — for example if someone claims “that’s not my signature”, they could be said to be “repudiating” the authenticity of the document. In the case of email, the impact is that if you have a copy of an email in its original format including full headers (for example, an email spool dump) you can check the signature. The extent to which this is a reliable means of verification varies depending on the circumstances — keys short enough to be cracked used to be common, and in some cases straight-up theft of the private keys is plausible.

Meanwhile, secure messaging tools like OTR and its successors have taken the approach of explicitly providing “deniable encryption”. The state of the art allows a sender, given a recipient’s public key, to craft a fake transcript apparently between the two of them that will pass cryptographic checks. This is generally fine for users of these apps because they know what they said. To the best of my knowledge, there is nowhere this creates a legal “get out of jail free” card. All it really does is ensure the users of these tools aren’t reducing their deniability by using the tool. This is an issue where DKIM really fails its users, and I’m apparently not the only one that feels this way.

Apropos of nothing, I really wish Gmail would start publishing its expired DKIM secret keys.

Matthew Green

A little over three years ago, I started doing exactly that for my domain. Since then, I’ve had a key rotation script running every day, generating a new key and adding the appropriate record (called a “selector”).

20170829-b29b2444f764c222c3faf5c._domainkey.ryanc.org. 5 IN TXT "v=DKIM1;t=s;h=sha256;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkOSIRW7R8a3e0J0lZqbBJSpHJYPk043/OB3lcT2apKtnu7MLjIRqUAgRyYSVAGC10ID2Qlxmy1Ji3EBRB1qI2IsNKgC2C4qzGxx54ShpVR/8yY9Qy1eyNtTF5Y/XSoLWoRVO1oly+WL+4O2TRuyujEwoZcFUwXzuuuqJtzbI17wIDAQAB"

Each selector remains live for seven days, then is “revoked” by publishing an update blanking the public key portion of the record.

20170829-b29b2444f764c222c3faf5c._domainkey.ryanc.org. 5 IN TXT "v=DKIM1;t=s;p="

Once another three days pass, the minimal set of RSA parameters needed to recreate the public and private keys are published in the selector’s “notes” field.

20170829-b29b2444f764c222c3faf5c._domainkey.ryanc.org. 5 IN TXT "v=DKIM1;t=s;p=;n=e:AQAB,p:6o/8upWykC5USot9Q2o5M89EO1qA7J/ao/FPc2TUJKat+z4JXde2HWW/8D3LJR4hGwSpgwLMq9drTzdjbzFTkQ==,q:+RTTux+yMx0LPyXDkAQiEBcOt8xYrr60s1sXO/5nQSQSZBlLtRJKHQpz65MnIxlOCB+1umqLW8q78hHC3Asxfw=="

The format here is non-standard, as a full RSA private key with all of the redundant data it includes would exceed the 255 character limit for strings stored in DNS[3]. A small Python script is enough to reconstitute everything, though.

dkim-private.py
1import gmpy2, sys, dns.resolver
2from Cryptodome.PublicKey import RSA
3from base64 import b64decode as b64d
4
5def decode_dkim_private(txt):
6 params = dict()
7 # Parse the DKIM selector record.
8 for key, _, val in map(lambda x: x.partition('='), txt.split(';')):
9 if key == 'n':
10 for k, v in map(lambda x: x.split(':'), val.split(',')):
11 params[k] = int.from_bytes(b64d(v), 'big')
12 # Compute rest of RSA keypair parameters (if possible).
13 if all (k in params for k in ('e', 'p', 'q')):
14 params['n'] = params['p'] * params['q']
15 phi = (params['p'] - 1) * (params['q'] - 1)
16 params['d'] = int(gmpy2.invert(params['e'], phi))
17 rsa = map(lambda x: params[x], 'nedpq')
18 return RSA.construct(tuple(rsa))
19 else:
20 return None
21
22if __name__ == '__main__' and len(sys.argv) == 3:
23 domain = sys.argv[1]
24 selector = sys.argv[2]
25 for answer in dns.resolver.query(selector + '._domainkey.' + domain, 'TXT'):
26 txt = str(answer).strip('"')
27 print(decode_dkim_private(txt).exportKey().decode())

An example run:

$ ./dkim-private.py 'ryanc.org' '20170829-b29b2444f764c222c3faf5c'
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDkOSIRW7R8a3e0J0lZqbBJSpHJYPk043/OB3lcT2apKtnu7MLj
IRqUAgRyYSVAGC10ID2Qlxmy1Ji3EBRB1qI2IsNKgC2C4qzGxx54ShpVR/8yY9Qy
1eyNtTF5Y/XSoLWoRVO1oly+WL+4O2TRuyujEwoZcFUwXzuuuqJtzbI17wIDAQAB
AoGBAKClArD7PzExKGJcIQqHIjqEzdfVdbVfyc+JfUiX72h2bE78wzXDUIUMYnrs
nJ7gJeaO5ycG5ST29sQtAkVRwn1KTLaU9fYmGpbkKyOWWfmztppZIvwi9l4tU5h2
GJVw+HbhcWO6tYbTqR9Bc8IelXyVibwmJwImr0AoD8sBLryhAkEA6o/8upWykC5U
Sot9Q2o5M89EO1qA7J/ao/FPc2TUJKat+z4JXde2HWW/8D3LJR4hGwSpgwLMq9dr
TzdjbzFTkQJBAPkU07sfsjMdCz8lw5AEIhAXDrfMWK6+tLNbFzv+Z0EkEmQZS7US
Sh0Kc+uTJyMZTggftbpqi1vKu/IRwtwLMX8CQFT/ABGMlTvxzdGFYkq/fyLrBEqN
rRIRiuTFWIj0DHuLepgEDtjWhcN5T2f6vFYi6NQliFdU+F18ngICjCGKukECQHse
ClIyJpkRQB/kgLfM8zFU1FeRUDx/0z3cRq3G4C7Yr6Z+wmcsNSoJoqbMw8mblnB5
jBAq3dtvaFsM4G53se0CQQC9ocR9eQdXvq5ibwZAmgYcMLEaq7NeX//l6zdxLd52
NcVcuaAUzf5KdTRwA9gJ4Qdzwntc+UB2ElpI2AOj7AFV
-----END RSA PRIVATE KEY-----

When I originally set this up, I was a bit concerned that I’d run into issues with filtering systems trying to validate my sent emails significantly after delivery. Per the RFC:

A signer should not sign with a private key when the selector containing the corresponding public key is expected to be revoked or removed before the verifier has an opportunity to validate the signature. The signer should anticipate that verifiers may choose to defer validation, perhaps until the message is actually read by the final recipient. In particular, when rotating to a new key pair, signing should immediately commence with the new private key and the old public key should be retained for a reasonable validation interval before being removed from the key server.

In the process of writing this up, I went through the 24 months of query logs I have. With very few exceptions (most of which were probably my own testing) there were no lookups against selectors other than on the day they were being used, so this doesn’t seem to be a problem in practice.

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.

It would make for a fascinating experiment for one of the privacy focused email providers to try deploying a key management strategy similar to the one I’ve described in this post.

[1]I can’t find any recent public data on this, but Google reported that 87.6% of non-spam emails received by Gmail users had valid DKIM signatures as of Febuary 2016. https://security.googleblog.com/2013/12/internet-wide-efforts-to-fight-email.html
[2]The core protocol behind email, SMTP was designed in the early eighties, and one of the terms it uses is “envelope sender”. This is apt because it originally was not much harder to fake than the return address on a physical envelope.
[3]The DNS standards provide for storing values longer than 255 characters in a TXT record by simply storing multiple strings in the record, but such records can be annoying to work with in some software.