git » chasquid » commit b09ef96

dkim: Limit size of RSA keys to 8192 bits

author Alberto Bertogli
2026-10-12 14:24:21 UTC
committer Alberto Bertogli
2026-10-12 14:36:02 UTC
parent 71af456be75e6605e66015f098c10340add4867d

dkim: Limit size of RSA keys to 8192 bits

Operating on large RSA keys can be computationally expensive. Several
implementations (e.g. crypto/tls, OpenSSL, rspamd) limit their size
to prevent DoS via work amplification.

This patch introduces that limit in our dkim library, set to 8192 bits.

That's the same limit used by other implementations, like crypto/tls
(which limits size of RSA keys in certificates) and rspamd (specifically
for DKIM). It is also in line with the most recent DMARC.org report of
key sizes in the wild (https://dmarc.org/stats/dkim/), where 8192 is
already rare, and the largest one found.

internal/dkim/dns.go +17 -0
internal/dkim/dns_test.go +36 -0

diff --git a/internal/dkim/dns.go b/internal/dkim/dns.go
index 8a2fdaa..ef03c86 100644
--- a/internal/dkim/dns.go
+++ b/internal/dkim/dns.go
@@ -167,6 +167,7 @@ var (
 	errInvalidRSAPublicKey = errors.New("invalid RSA public key")
 	errNotRSAPublicKey     = errors.New("not an RSA public key")
 	errRSAKeyTooSmall      = errors.New("RSA public key too small")
+	errRSAKeyTooLarge      = errors.New("RSA public key too large")
 	errInvalidEd25519Key   = errors.New("invalid Ed25519 public key")
 )
 
@@ -192,6 +193,22 @@ func parseRSAPublicKey(p []byte) (verifyFunc, error) {
 		return nil, errRSAKeyTooSmall
 	}
 
+	// Enforce 8192-bit maximum.
+	//
+	// The RFC requires verifiers to handle keys of 1024 to 4096 bits, and
+	// does not set a maximum. But the key is attacker-controlled, and the
+	// cost of verifying grows quickly with its size: a key that fits in a
+	// TXT record can take seconds to verify.
+	//
+	// 8192 is chosen to be well above what legitimate signers use:
+	//   - DMARC.org saw 277 keys of 8192 bits in 2025 (out of ~11.8M), and
+	//     none larger.
+	//   - rspamd and Go's crypto/tls (for certificates) use the same limit.
+	// https://datatracker.ietf.org/doc/html/rfc8301#section-3.2
+	if rsaPub.Size()*8 > 8192 {
+		return nil, errRSAKeyTooLarge
+	}
+
 	return func(h crypto.Hash, hashed, signature []byte) error {
 		return rsa.VerifyPKCS1v15(rsaPub, h, hashed, signature)
 	}, nil
diff --git a/internal/dkim/dns_test.go b/internal/dkim/dns_test.go
index 6615224..4899604 100644
--- a/internal/dkim/dns_test.go
+++ b/internal/dkim/dns_test.go
@@ -3,8 +3,11 @@ package dkim
 import (
 	"context"
 	"crypto"
+	"crypto/rsa"
+	"crypto/x509"
 	"encoding/base64"
 	"errors"
+	"math/big"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
@@ -184,6 +187,39 @@ func TestParsePublicKey(t *testing.T) {
 	}
 }
 
+func TestRSAKeySize(t *testing.T) {
+	// Builds an RSA public key record with a modulus of the given size. The
+	// key doesn't need to be real, as long as it can be parsed.
+	keyRecord := func(bits int) string {
+		n := new(big.Int).Lsh(big.NewInt(1), uint(bits-1))
+		n.SetBit(n, 0, 1)
+		der, err := x509.MarshalPKIXPublicKey(&rsa.PublicKey{N: n, E: 65537})
+		if err != nil {
+			t.Fatalf("error marshalling %d-bit key: %v", bits, err)
+		}
+		return "p=" + base64.StdEncoding.EncodeToString(der)
+	}
+
+	cases := []struct {
+		bits int
+		err  error
+	}{
+		{1024, nil},
+		{2048, nil},
+		{4096, nil},
+		{8192, nil},
+		{8193, errRSAKeyTooLarge},
+		{16384, errRSAKeyTooLarge},
+		{262144, errRSAKeyTooLarge},
+	}
+	for _, c := range cases {
+		_, err := parsePublicKey(keyRecord(c.bits))
+		if !errors.Is(err, c.err) {
+			t.Errorf("%d bits: want %v, got %v", c.bits, c.err, err)
+		}
+	}
+}
+
 func TestPublicKeyMatches(t *testing.T) {
 	cases := []struct {
 		pk *publicKey