From b09ef96eab68114aab0607179018c71c8046637a Mon Sep 17 00:00:00 2001 From: Alberto Bertogli Date: Mon, 12 Oct 2026 15:24:21 +0100 Subject: [PATCH] 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 +++++++++++++++++ internal/dkim/dns_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) 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