Fun with DKIM
DKIM signing and verification look mostly like an opaque black box from the outside, without any easy way to look at intermediate steps and understand what’s going on. Perhaps we can do something, though.
We really can’t do the RSA cryptography by hand, but let’s see how far we can get.
Canonicalization
The first step in DKIM signing a message is to canonicalize and hash
the body. Canonicalization is the process of tidying up whitespace, so that
trivial changes as the message is sent don’t change the body hash. There
are two sorts of canonicalization - simple and relaxed. You’ll see these
listed as relaxed/simple and relaxed/relaxed - the “relaxed” before the
slash is describing how the headers are canonicalized, which we don’t
care about right now.
Simple body canonicalization chops off any trailing blank lines, and if what’s left doesn’t finish with a CRLF, adds a CRLF to the end.
“CRLF” is short for carriage return + line feed, and means the sequence of two bytes that’s used to end lines in email. The bytes are 10 and 13, often written as “\r\n” and universally just called a CRLF.
Relaxed body canonicalization does a bit more. It removes all whitespace at the end of each line, collapses any sequence of whitespace in a line into a single space and removes any trailing blank lines. What’s left is the relaxed canonicalized body.
Both of these are described in RFC 6376, sections 3.4.3 and 3.4.4.
The simplest case for calculating a body hash is an email with an empty body, so let’s do that.
Simple canonicalization will convert it an empty body just a CRLF, two bytes long, while relaxed canonicalization will convert it to an empty string, zero bytes long.
Hashing
Once we have the canonicalized body we need to hash it, creating a fingerprint that will change if any part of the body is changed. To do this we take a SHA256 hash of the (canonicalized) body and convert it to a base64 encoded string.
We can do that like this, for the simple canonicalization of our empty body:
echo -n "\r\n" | openssl sha256 -binary | base64
“echo -n” just takes the string it’s given - our CRLF here - and prints it. The “-n” tells echo not to add a newline, just to print what it’s given. And “\r\n” is the way we type a CRLF into the shell.
“openssl sha256 -binary” takes it’s input, runs it through the SHA256 hashing algorithm and prints the result in binary (i.e. not encoded into hex digits, which would be the format you’d typically see).
“base64” then takes that binary hash and encodes it with base64 encoding. That gives us this, our simple body hash:
frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=
And we can do the same thing for our relaxed canonicalization:
echo -n "" | openssl sha256 -binary | base64
That gives us this, our relaxed body hash for an empty body:
47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
This body hash will be stored in the bh= field of the DKIM-Signature
header of the mail.
Really, that’s how it works
Send yourself an email with an empty body, maybe to your gmail account. Look at the headers of the mail and find the DKIM-Signature header.
It’ll look something like this:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=blighty.com;
s=blueberry; t=1793116010;
bh=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=;
h=From:Subject:Date:To:From:Subject:Date:To:Reply-To:Cc;
b=i0ZHnwEPNsdOrsf5vWDaIuqKCi7TWPIjFLlh2FtGqQHQEboXFtDkNBSHrFmPizi57
e9sdrZRlFs4qNE6p87vTEMrjgxn4ar/aGxblRX8kh27wEbmO10eGBMD+uQtPa0poMr
N0gjTaynR6XkgapD32VgMXOL8H+6UWBORA3qYgZkDeMGPq1n5l+esZWyfV6ox4szpx
4yDIt1X5+CNKLGAI+7XHwHyZgX6dldcYGGlMLuM8j6xt95HxfzDnaJzjwZmRSb8YMM
P85a+0L601L4ez9QbWLk/Q2LwG2Ji++cYACYAzbWvZ7bc2tYqLXZJ5inFaE+nxxuZ7
8LnArgYh/ys5w==
We can see a=rsa-sha256 which confirms that we’ve used SHA256 to hash
everything.
We see c=relaxed/simple which tells us that we’ve used simple
canonicalization for the body.
And bh=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=, which is the simple
body hash for an empty body we calculated earlier.
It really works.
An empty body is interesting
An empty body is simple to calculate, and it’s the one case where the expected hash is in the DKIM RFC so you can be sure you’ve got the right answer.
And body canonicalization is something DKIM code sometimes get wrong, particularly with the different handling of trailing CRLFs between simple and relaxed canonicalization. Checking that the body hash of an empty email is correct is a good smoke test of newly written or configured DKIM signing code.