skip to content

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

< PHP

Post your comment or question
top