| author | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-10-09 11:28:58 UTC |
| committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-10-09 20:34:17 UTC |
| parent | 3b0836f811b71d7330fb887baf7f43beba763c65 |
| internal/dkim/message.go | +5 | -1 |
| internal/dkim/message_test.go | +11 | -0 |
| internal/dkim/sign_test.go | +41 | -0 |
diff --git a/internal/dkim/message.go b/internal/dkim/message.go index 0cc31dd..31dd341 100644 --- a/internal/dkim/message.go +++ b/internal/dkim/message.go @@ -18,7 +18,11 @@ type headers []header func (h headers) FindAll(name string) headers { hs := make(headers, 0) for _, header := range h { - if strings.EqualFold(header.Name, name) { + // Compare using strings.ToLower and not strings.EqualFold, to be + // consistent with the verifier (see headersToInclude). They differ + // for some non-ASCII characters, which are invalid in header names + // but can still appear in messages. + if strings.ToLower(header.Name) == strings.ToLower(name) { hs = append(hs, header) } } diff --git a/internal/dkim/message_test.go b/internal/dkim/message_test.go index 1425832..9023231 100644 --- a/internal/dkim/message_test.go +++ b/internal/dkim/message_test.go @@ -182,4 +182,15 @@ func TestHeadersFindAll(t *testing.T) { t.Errorf("headers.Find() mismatch (-want +got):\n%s", diff) } + // "\u017fubject" (U+017F is a long s) must not match "Subject", even + // though they are equal under Unicode case folding. + hs = append(hs, header{Name: "\u017fubject", Value: "x"}) + subjectHs := hs.FindAll("SUBJECT") + expected = headers{ + {Name: "Subject", Value: "test", Source: "Subject: test"}, + } + if diff := cmp.Diff(expected, subjectHs); diff != "" { + t.Errorf("headers.Find() mismatch (-want +got):\n%s", diff) + } + } diff --git a/internal/dkim/sign_test.go b/internal/dkim/sign_test.go index f31e43b..f5b0239 100644 --- a/internal/dkim/sign_test.go +++ b/internal/dkim/sign_test.go @@ -210,6 +210,47 @@ Hi. } } +func TestSignNonASCIIHeaderNames(t *testing.T) { + // Header names with non-ASCII characters are invalid, but we must still + // sign them consistently with how we verify them. + // Here, "\u017fubject" (U+017F is a long s) is not the same as + // "Subject", and should not be signed as such. + ctx := t.Context() + ctx = WithTraceFunc(ctx, t.Logf) + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("ed25519.GenerateKey: %v", err) + } + + ctx = WithLookupTXTFunc(ctx, 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, + } + + message := toCRLF("From: from@example.com\n" + + "\u017fubject: Not a subject\n" + + "\n" + + "Hi.\n") + + sig, err := s.Sign(ctx, message) + if err != nil { + t.Fatalf("Sign: %v", err) + } + + res, err := VerifyMessage(ctx, addSig(sig, message)) + if err != nil || res.Valid != 1 { + t.Errorf("VerifyMessage: wanted 1 valid / nil; got %v / %v", res, err) + } +} + func addSig(sig, message string) string { return "DKIM-Signature: " + strings.ReplaceAll(sig, "\r\n", "\r\n\t") +