Skip to content

Signing

Stalwart signs outgoing messages with DKIM so that receiving servers can verify two things: that the signing domain sent the message, and that the message was not modified in transit. Signing happens in two decisions. First, a signing domain is chosen for each message. Then the keys of that domain sign it. DKIM overview introduces signatures, selectors and alignment.

The signing domain is chosen by dkimSignDomain, an expression of the DATA stage on the MtaStageData singleton (found in the WebUI under Settings › MTA › Session › DATA Stage). The expression returns the name of a domain, whose keys then sign the message, or false for no signature. The default expression is:

{
"dkimSignDomain": {
"match": {
"0": {"if": "is_local_domain(sender_domain) && !is_empty(authenticated_as)", "then": "sender_domain"}
},
"else": "false"
}
}

This expression has two conditions. A message is signed only when the client authenticated, and only when the domain of the envelope sender is hosted on the server. With the default expression, the following messages are therefore sent unsigned:

  • Messages relayed for an application server or another system that delivers to Stalwart without authenticating, even when their sender is in a local domain
  • Messages whose envelope sender is in a domain that is not hosted on the server

A trusted application server that relays without authentication is a common case. To sign its messages, add a branch for its address:

{
"dkimSignDomain": {
"match": {
"0": {"if": "is_local_domain(sender_domain) && (!is_empty(authenticated_as) || remote_ip == '192.0.2.10')", "then": "sender_domain"}
},
"else": "false"
}
}

The expression returns a single domain. The message is then signed with every DkimSignature of that domain whose stage is active, both DKIM and DKIM2 keys. The two kinds combine differently. A domain with an Ed25519 key and an RSA key adds two DKIM signatures to each message, one per key. Its DKIM2 keys, in contrast, share a single DKIM2 signature with one entry per key.

Messages that Stalwart generates itself do not use this expression, because each kind has its own signing settings:

Each of these has a dkimSignDomain expression of its own. These messages carry no DKIM2 chain, so their DKIM2 signature starts one. That signature is added only when their From header field is Authentic for the signing domain (Reports and notifications).

A message from an address in an alias domain is signed with the keys of the domain it is an alias of, and that domain appears in the d= tag. The signature therefore does not align with the From domain for DMARC. When the mail of a domain must pass DMARC with DKIM alignment, that domain has to be a separate Domain object with its own keys (Domain aliases).

Each key is a DkimSignature (found in the WebUI under Management › Domains › DKIM Signatures) that belongs to a Domain. The variant of the object selects the kind of signature and the algorithm:

VariantSignatureAlgorithm
Dkim1Ed25519Sha256DKIMEd25519 with SHA-256 (RFC 8463)
Dkim1RsaSha256DKIMRSA with SHA-256 (RFC 6376)
Dkim2Ed25519Sha256DKIM2Ed25519 with SHA-256
Dkim2RsaSha256DKIM2RSA with SHA-256
Dkim2MlDsa44Ed25519Sha512DKIM2ML-DSA-44 and Ed25519 composite with SHA-512 (post-quantum)

RSA-SHA1 is not available for signing, because RFC 8301 prohibits it. Use RSA keys of at least 2048 bits. Receivers ignore RSA signatures made with keys shorter than 1024 bits, as RFC 8301 requires. Signing with both Ed25519 and RSA lets receivers that do not support Ed25519 verify the RSA signature. The DKIM2 variants, their fields and the choice between their algorithms are described in DKIM2.

Most domains do not need keys defined by hand. With dkimManagement set to Automatic on the Domain, Stalwart creates the keys. When the domain also uses automatic DNS management, Stalwart publishes and rotates them as well. DKIM key rotation describes the settings and the stages of a key. Without automatic DNS management, the keys sign mail at once but are not published or rotated. Receivers then cannot verify the signatures until the TXT records listed in the domain’s dnsZoneFile are added to the zone by hand.

A key is created by hand in two cases: when dkimManagement is set to Manual, and when a key must be shared with another sending system. The fields of the DKIM variants are:

FieldPurpose
domainIdRequired. The domain the key belongs to.
selectorRequired. The selector, which names the DNS record of the public key, <selector>._domainkey.<domain>.
privateKeyRequired. The private key, given directly, from an environment variable or from a file. RSA keys must be in PEM (PKCS#1 or PKCS#8), and Ed25519 keys may be in PEM or bare base64 PKCS#8, as may the composite keys of DKIM2 (DNS records).
stageOnly active keys sign. pending, retiring and retired are rotation stages.
headersThe header fields covered by the signature, by default Date, From, Message-ID, Subject and To.
canonicalizationHow headers and body are normalised before hashing.
expireValidity of each signature, in milliseconds, written to the x= tag. Without it, signatures do not expire.
reportAsk receivers for failure reports, with the r=y tag (RFC 6651).
auidThe agent or user identifier, the i= tag.
thirdParty, thirdPartyHashThe authorised third-party signature of RFC 6541.

The public key is not supplied. The server derives it from the private key and shows it in publicKey. The following object defines an RSA key read from a file, covering additional header fields:

{
"@type": "Dkim1RsaSha256",
"domainId": "<Domain id>",
"selector": "rsa-2026",
"privateKey": {"@type": "File", "filePath": "/etc/stalwart/dkim/rsa-2026.key"},
"headers": {
"From": true, "To": true, "Cc": true, "Date": true, "Subject": true,
"Message-ID": true, "MIME-Version": true, "Content-Type": true,
"In-Reply-To": true, "References": true, "List-Id": true
},
"stage": "active"
}

The following commands generate an RSA key of 2048 bits and an Ed25519 key, in PEM:

Terminal window
openssl genrsa -out rsa.key 2048
openssl genpkey -algorithm ed25519 -out ed25519.key

Receivers find the public key in a TXT record at <selector>._domainkey.<domain>, where the value of the p= tag is the base64 public key. For the RSA key, the following command produces that value:

Terminal window
openssl rsa -in rsa.key -pubout -outform der 2>/dev/null | openssl base64 -A

An Ed25519 key is published as the raw 32-byte key, which the following command extracts:

Terminal window
openssl pkey -in ed25519.key -pubout -outform der | tail -c 32 | openssl base64 -A

The records then read as follows, with k=ed25519 for Ed25519 keys:

rsa-2026._domainkey.example.org. IN TXT "v=DKIM1; k=rsa; p=<base64 key>"
ed-2026._domainkey.example.org. IN TXT "v=DKIM1; k=ed25519; p=<base64 key>"

The record of a 2048-bit RSA key is longer than 255 characters, so it must be split into several strings within one TXT record. The domain’s dnsZoneFile also lists the record of each key, ready to copy.