PHP: Generating a Key Pair for DKIM
As you are by now probably aware, Google and Yahoo! in 2024 have started requiring DMARC authentication/alignment for large senders. For now this means that your emails must pass either SPF or DKIM – so DKIM remains optional there.
But some ISPs, including GMX in Europe, are already requiring DKIM alignment for inbound emails and it's a matter of time before others follow.
Here we are documenting the first step to DKIM alignment which involves generating a public/private key pair and adding a TXT record to your DNS.
DKIM Basics
What you need for DKIM implementation is a private key to be stored securely on your website – somewhere readable by PHP, but not accessible to the public. And A DNS TXT entry to publish your public key to the world.
The sending process then involves signing outbound emails using your private key, and the recipient verifying the validity of the DKIM signature using the public key which they can fetch through a DNS lookup. More on that later…
A single domain can have multiple DKIM keys, each identified by a selector which is just a short text string. You will see this used in both the signing and validation process.
The required DNS entry will look something like this:
website._domainkey.example.net IN TXT "v=DKIM1; k=rsa; p=..."
Where "website" in this case is your chosen selector and "example.net" your domain.
Generating DKIM keys from the command-line
If you are proficient with command line tools you can generate a private key and the associated DNS TXT entry as follows:
# SELECTOR=website
# KEYNAME=mydomain-$SELECTOR.key
# openssl genrsa -out $KEYNAME 2048
# openssl rsa -in $KEYNAME -pubout -outform der 2>/dev/null \
| openssl base64 -A \
| sed -r 's/(.{218})/\1 /g' \
| awk -v sel="$SELECTOR" '{print sel"._domainkey\tIN\tTXT\t\"v=DKIM1; k=rsa; p="$1"\" \""$2"\""}' \
> bind-public-key.txt
The above commands should be run as root, or using sudo, to keep the private key secure.
This will deliver you two text files. The first being your private key:
mydomain-website.key:
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9v0BAQEFAASCBKkvggSlAgEAAoIBAQCdmGKLqqvNUv8i
v0YHUYTDDLWQkG8cZ+EJF5ulk8d75EMfP49ZyseEUuEviYrf/eolxjVVFqGxJVH/
8OKve4lLC5J+0vajSRbvWLMHX/gvboBRlgehI+YxvvBpMZkf5nSnhoplOpnbNR1o
ufDIJ/BUU73M7gTEZXH9bkaMB8ideouVy88E7FrWQkvv8d4NuJHV8jebbafRb21f
DJ+HC4tb2Y4v9qWDFD2/OR3LLklg52W4eiJkVLf4agfpicZh8QTWY+lk1PXL7pFz
sGYkNBNVUcqD0Sqr8Uzrap95klAPmNHOPdKtp+95nLDcvM4eN4Bo3+34jCRGLlvj
9vcGhO09AgMBAAECggEAPwtqhNgKcZG9ykDXH29fjo3jhokZQgJWdoYwuGzldS0M
IxCQvsmNxmRHfNzRJylTRbhEtpzeo1i8NIi/jv1kn5ZqDP1Lnv/Kwuyg9hbQ2UMj
Zz//HloXqRmEhniWern9OdVrQPLQAO7/LFmSNugvTvTPLY+cbZrtnoZCh5tHiKOQ
B8JxxEi2LA9ozuvtsVmM9I4uwzIbAn2Vx4TXqejR3UsuV6B9ApAbmMesnQ9mIpSp
iVrIGxqF9U56DXaXSHlWVnPrYDFRFmuJRHId+osjGmzvK3ddzxq/nNgGrXRHb1LU
Mw9SMjtjWWYeV4WpNa3M5zeFUvpm72cLIDIQ+CGQOwKBgQDUM0EecnBrXrycnk0i
99+0WhdK2kfELo0sUJKUEJz4R6tBPJK/mPuuA0LzhV2OhRhMw0quR5zMuHZgruHa
YoXmPhyewmFhl2KPByxDNjfLn9t4WVhxDHKxXsi4A6ySLNOg3/TyEnweiYgqLCrN
0gSS4TMxsObw3bEMobrhFsUxjwKBgQC+H8hGJGKSmCd+f80t0y2GaHnqpYQORZpX
TwsJHd+L+Vy/tWV6hjgKYcd3s+Kd1D0VVcC8EBO6kNnd3KboqZtqmoCU2CzXpWnI
fdCmzVd6lMP3k7Zy0PX3ZJ8Ezo9mhuiQDW7iEL6LyvS7DOIzClAlsK5vIBPVHpUZ
98IcOAy2cvKBgQDHURVbefaqg6P6IJ8nt1hC2VSDlKBQX8Fu3Iex2CD4/KiZcEIP
Aa11d87NWnnUQqPehpmBNfbMPH/EtL+kF2LaL4FGgmCKAF4tJnmm8ChcdVz6oEF4
fk7E19kFLz5LVxu5QmObdU1siZaCtlXGWfy90hX6GMXzfOiuisM0ZeT3dQKBgQC9
4WYWz433FKkVCLS1mJx2CXABrm62BkPAAPxnjYNO+6vq91KzTMs5azBY17pzoJ2k
6jEEYhYiFTrR/uZfpczHaikS/tfCQ7zjdOxnOtusXFlfsRHdl96fxsmeZmc3oXMx
M4lTlB+J5BgJnDNpgFpNWijMaUAFcHa/KZeesUfZCvKBgQCy/3Y0aSJorXvGMMkV
eToRS3JzrWL4htKAavQpigEWWoo43XPs+RVOqnT0ipc6R5lukIvv/+Fos8BQTr9T
NakYdnoBXBiyj3e4+kld1C4tyasaGiH3SoyQUhgtsPqYSHvQ5rYKfs7/fJ97pyas
I6GHLRab9KA/QYCiWkPMAZy63v==
-----END PRIVATE KEY-----
If you want you can also have your private key encrypted with a passphrase, which then requires decryption every time for the signing process. That is not covered here.
and the second being a BIND TXT entry to be added to your DNS:
bind-public-key.txt:
website._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9v0BAQEFAAOCAQ8AMIIBCgKCAQEAnZhii6qrzVL/IsNGB1GEvwy1kJBvHGfhCRebpZPHe+RDHz+PWcrHhFLhMImK3/3qJcY1VRahsSVR//DisHuJSvuSftMGo0kW8FizB1/4MG6AUZYHoSPmMcLvaTGZH+Z0p4aKZTqZ2zUdaLnwyCfwVFO9zO4ExGVx/W5GjAfInXqLlc" "vPBOxa1kJML/HeDbiR1fI3m22n0W9tXwyfhwuLW9mOMPalgxQ9vzkdyy5JYOdluHoiZFS3+GoH6YnGYfEE1mPpZNT1y+6Rc7BmJDQTVVHKg9Eqq/FM62qfeZJQD5jRzj3SrafveZyw3LzOHjeAaN/t+IwkRi5cI/cHBoTtPQIDAQAB"
Note that we are splitting the TXT record into two parts here in order to keep each part shorter than the 255 character limit present in some name servers. Both keys can be longer or shorter if the bit length is changed.
This needs to be added to your domain's DNS as a TXT record. If you are using cPanel or similar you probably just need the first and last parts and to select TXT as the record type.
Generating DKIM using PHP
From PHP we can use the built-in OpenSSL library to replicate the same process as the command-line example above.
<?PHP
namespace Chirp;
define('KEY_BITS', 2048); // bit length
define('KEY_TYPE', OPENSSL_KEYTYPE_RSA);
function key_to_dns(string $public_key, string $selector = NULL) : string
{
// convert a public key into a BIND TXT entry for DKIM
$key_content = explode(PHP_EOL, trim($public_key));
$key_content = array_filter($key_content, fn($val) => !preg_match("/^-+/", $val));
$key_content = str_split(implode("", $key_content), 218);
$retval = ($selector) ? "{$selector}._domainkey" : "@";
$retval .= "\tIN\tTXT\t\"v=DKIM1; k=rsa; p=" . implode('" "', array_filter($key_content)) . "\"";
return $retval;
}
function gen_key(string $selector = NULL) : array
{
// returns an array containing [PRIVATE_KEY, BIND_TXT_ENTRY]
$pkey_res = openssl_pkey_new([
'private_key_bits' => KEY_BITS,
'private_key_type' => KEY_TYPE
]);
openssl_pkey_export($pkey_res, $private_key);
$public_key = key_to_dns(
openssl_pkey_get_details($pkey_res)['key'],
$selector
);
return [
$private_key,
$public_key
];
}
You can use these functions to generate a key pair as follows:
<?PHP
list($private_key, $public_key) = \Chirp\gen_key("website");
Note that the public key is extracted from the private key so it's important that they're generated and installed together. That is why our function returns both together in an array.
You could extend the above code to write the private key to a local file, or even to directly update the DNS. On our system those are manual processes requiring root access for enhanced security.
Permissions on the private key should be set for read-only access by the web server user, with the file owned by root. And to be compatible with our DKIM-signing PHP class the keys need to be accessible under the PHP include_path at ~/dkim-keys/mydomain-website.key
Options for DKIM signing
We have developed a PHP class for adding DKIM headers to outgoing emails that are sent using the built-in mail function. Existing PHP classes such as PHPMailer already have this capacity built in.
Aside from PHP, other options for signing outgoing emails include installing a DKIM 'milter' in your mail transfer agent (MTA). If you do this you will need to feed it an array of domains to handle, the matching private key locations, and which selector to use in each case.
In sendmail the previous dkim-milter has been replaced by OpenDKIM which can be installed as a package (opendkim, opendkim-tools) in most distributions. Installation and configuration instructions for sendmail can be found here.
If you have a DKIM *._domainkey already in your DNS that was provided to you by GMail, Outlook or another email service provider (ESP) you will not be able to use it for local signing of outgoing emails as they retain the associated private key. You will need to generate your own key pair with a different selector.
References
- Wikipedia: DomainKeys Identified Mail
- OpenDKIM
Related Articles - Sendmail
- PHP Signing outbound emails with DKIM
- PHP Generating a Key Pair for DKIM
- System Using qtool.pl to manage sendmail queues
- System DKIM Key Pair Generator
- System Analysing the mail.log
- System Analysing mailq and the mqueue directory
- System Expanding IPv6 Addresses for DNSBL Checks