git » chasquid » commit 4b0b31a

dkim: Add fuzzing tests

author Alberto Bertogli
2026-10-09 20:09:21 UTC
committer Alberto Bertogli
2026-10-09 20:34:17 UTC
parent b3b1f39d855c7f26e95127626db1b1e8be317bcb

dkim: Add fuzzing tests

This patch adds two fuzzing tests to the dkim package:

1) Fuzz the sign-verify cycle, checking we can verify any message we
   sign.
2) Fuzz the message we verify, returning valid keys for any domain so it
   gets past the key fetching, and exercises a substantial part of the
   verification process.

The first test already found two issues, which were fixed in the
previous patches.

internal/dkim/file_test.go +17 -0
internal/dkim/sign_test.go +55 -0
internal/dkim/verify_test.go +35 -0

diff --git a/internal/dkim/file_test.go b/internal/dkim/file_test.go
index 443f26d..dfbc7d4 100644
--- a/internal/dkim/file_test.go
+++ b/internal/dkim/file_test.go
@@ -86,6 +86,23 @@ func equalErrors(a, b error) bool {
 	return a.Error() == b.Error()
 }
 
+// Add the messages from the file-based tests to the fuzzing corpus.
+func addTestdataCorpus(f *testing.F) {
+	f.Helper()
+
+	msgfs, err := filepath.Glob("testdata/*.msg")
+	if err != nil {
+		f.Fatalf("error finding test files: %v", err)
+	}
+	for _, msgf := range msgfs {
+		c, err := os.ReadFile(msgf)
+		if err != nil {
+			f.Fatalf("error reading %q: %v", msgf, err)
+		}
+		f.Add(toCRLF(string(c)))
+	}
+}
+
 func mustReadFile(t *testing.T, path string) string {
 	t.Helper()
 	contents, err := os.ReadFile(path)
diff --git a/internal/dkim/sign_test.go b/internal/dkim/sign_test.go
index f5b0239..05a8756 100644
--- a/internal/dkim/sign_test.go
+++ b/internal/dkim/sign_test.go
@@ -1,6 +1,7 @@
 package dkim
 
 import (
+	"context"
 	"crypto/ecdsa"
 	"crypto/ed25519"
 	"crypto/elliptic"
@@ -376,3 +377,57 @@ func TestFormatHeaders(t *testing.T) {
 		}
 	}
 }
+
+func FuzzSignAndVerify(f *testing.F) {
+	f.Add(basicMessage)
+	f.Add("From: a@b\r\n\r\n")
+	f.Add("")
+
+	// Repeated headers with different values.
+	f.Add("From: a@b\r\nTo: c@d\r\n e@f\r\nTo: g@h\r\n\r\nbody\r\n")
+
+	// Non-ASCII header name (U+017F is a long s).
+	f.Add("From: a@b\r\nſubject: x\r\n\r\nbody\r\n")
+
+	// Messages from the file-based tests, which have more realistic headers,
+	// including existing DKIM signatures.
+	addTestdataCorpus(f)
+
+	// Use a fixed key, as generating one for each run would be too slow.
+	priv := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize))
+	pub := priv.Public().(ed25519.PublicKey)
+	ctx := WithLookupTXTFunc(context.Background(),
+		makeLookupTXT(map[string][]string{
+			"test._domainkey.example.com": {
+				"v=DKIM1; k=ed25519; p=" +
+					base64.StdEncoding.EncodeToString(pub),
+			},
+		}))
+
+	s := &Signer{
+		Domain:   "example.com",
+		Selector: "test",
+		Signer:   priv,
+	}
+
+	f.Fuzz(func(t *testing.T, msg string) {
+		sig, err := s.Sign(ctx, msg)
+		if err != nil {
+			return
+		}
+
+		// Any message we can sign, we must be able to verify.
+		// Our signature is the first one, so it's always verified even if
+		// the message has other signatures.
+		res, err := VerifyMessage(ctx, addSig(sig, msg))
+		if err != nil {
+			t.Fatalf("VerifyMessage error: %v", err)
+		}
+		if len(res.Results) == 0 {
+			t.Fatalf("signature not found when verifying")
+		}
+		if r := res.Results[0]; r.State != SUCCESS {
+			t.Errorf("signature did not verify: %v / %v", r.State, r.Error)
+		}
+	})
+}
diff --git a/internal/dkim/verify_test.go b/internal/dkim/verify_test.go
index 01063f8..6b92b77 100644
--- a/internal/dkim/verify_test.go
+++ b/internal/dkim/verify_test.go
@@ -520,3 +520,38 @@ func TestAuthenticationResults(t *testing.T) {
 		}
 	}
 }
+
+func FuzzVerifyMessage(f *testing.F) {
+	addTestdataCorpus(f)
+
+	// Return valid keys for every domain, so we can get past the key lookup
+	// and exercise the rest of the verification. The ed25519 key is the one
+	// used by testdata/01-rfc8463.
+	ctx := WithLookupTXTFunc(context.Background(),
+		func(ctx context.Context, domain string) ([]string, error) {
+			return []string{
+				"v=DKIM1; k=ed25519; p=" + exampleEd25519KeyB64,
+				"v=DKIM1; k=rsa; p=" + exampleRSAKeyB64,
+			}, nil
+		})
+
+	f.Fuzz(func(t *testing.T, msg string) {
+		res, err := VerifyMessage(ctx, msg)
+		if err != nil {
+			return
+		}
+
+		if res.Found != uint(len(res.Results)) {
+			t.Errorf("found %d signatures, but got %d results",
+				res.Found, len(res.Results))
+		}
+		if res.Valid > res.Found {
+			t.Errorf("%d valid signatures, but only %d found",
+				res.Valid, res.Found)
+		}
+
+		// This is always called on the result when receiving a message, so
+		// exercise it too.
+		res.AuthenticationResults()
+	})
+}