| author | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-10-09 20:09:21 UTC |
| committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-10-09 20:34:17 UTC |
| parent | b3b1f39d855c7f26e95127626db1b1e8be317bcb |
| 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() + }) +}