1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2026-10-29 15:02:30 +00:00
Files
go-chasquid-smtp/internal/dkim/message_test.go
T
Alberto Bertogli b3b1f39d85 dkim: Use same header name folding for signing and verifying
Today, we fold header names with strings.EqualFold when signing, but
strings.ToLower when verifying.

This should be harmless for valid messages (that have only ASCII
characters in the header names), but if one contains an (invalid) header
with a non-ASCII character, we could fail to verify the message we sign.

The inconsistency would only happen if a header has a non-ASCII
character that strings.EqualFold and strings.ToLower handle differently
(like "ſubject", with U+017F).

To make signing and verifying consistent, this patch makes signing use
strings.ToLower.

This bug was found by fuzzing the sign-verify cycle, which will be added
in a later patch.
2026-10-09 21:34:17 +01:00

197 lines
5.1 KiB
Go

package dkim
import (
"strings"
"testing"
"blitiri.com.ar/go/chasquid/internal/normalize"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestParseMessage(t *testing.T) {
cases := []struct {
message string
headers headers
body string
}{
{
message: normalize.StringToCRLF(`From: a@b
To: c@d
Subject: test
Continues: This
continues.
body`),
headers: headers{
header{Name: "From", Value: " a@b",
Source: "From: a@b"},
header{Name: "To", Value: " c@d",
Source: "To: c@d"},
header{Name: "Subject", Value: " test",
Source: "Subject: test"},
header{Name: "Continues", Value: " This\r\n continues.",
Source: "Continues: This\r\n continues."},
},
body: "body",
},
{
// Multiple continuation lines, with spaces and tabs, in a header
// that is not the last one. Colons in continuation lines are
// part of the value.
message: normalize.StringToCRLF(`From: a@b
Multi: one
two:three
four
To: c@d
body`),
headers: headers{
header{Name: "From", Value: " a@b",
Source: "From: a@b"},
header{Name: "Multi",
Value: " one\r\n two:three\r\n\tfour",
Source: "Multi: one\r\n two:three\r\n\tfour"},
header{Name: "To", Value: " c@d",
Source: "To: c@d"},
},
body: "body",
},
{
// Empty message.
message: "",
headers: headers{},
body: "",
},
}
for i, c := range cases {
headers, body, err := parseMessage(c.message)
if diff := cmp.Diff(c.headers, headers); diff != "" {
t.Errorf("parseMessage([%d]) headers mismatch (-want +got):\n%s",
i, diff)
}
if diff := cmp.Diff(c.body, body); diff != "" {
t.Errorf("parseMessage([%d]) body mismatch (-want +got):\n%s",
i, diff)
}
if err != nil {
t.Errorf("parseMessage([%d]) error: %v", i, err)
}
}
}
func TestParseMessageWithErrors(t *testing.T) {
cases := []struct {
message string
err error
}{
{
// Continuation without previous header.
message: " continuation.",
err: errInvalidHeader,
},
{
// Header without ':'.
message: "No colon",
err: errInvalidHeader,
},
{
// Header without ':' in its first line, even if there's one in
// a continuation line.
message: "No colon\r\n but: here",
err: errInvalidHeader,
},
}
for i, c := range cases {
_, _, err := parseMessage(c.message)
if diff := cmp.Diff(c.err, err, cmpopts.EquateErrors()); diff != "" {
t.Errorf("parseMessage([%d]) err mismatch (-want +got):\n%s",
i, diff)
}
}
}
func TestParseMessageWithoutBlankLine(t *testing.T) {
// If there's no blank line, all the lines are headers, including the
// continuation lines of the last one, and the body is empty.
hs, body, err := parseMessage("From: a@b\r\nTo: c@d\r\n e@f")
if err != nil {
t.Fatalf("parseMessage error: %v", err)
}
want := headers{
{Name: "From", Value: " a@b", Source: "From: a@b"},
{Name: "To", Value: " c@d\r\n e@f", Source: "To: c@d\r\n e@f"},
}
if diff := cmp.Diff(want, hs); diff != "" {
t.Errorf("parseMessage headers mismatch (-want +got):\n%s", diff)
}
if body != "" {
t.Errorf("parseMessage body: wanted empty, got %q", body)
}
}
func TestParseMessageManyContinuations(t *testing.T) {
// Regression test for parsing being quadratic on the number of
// continuation lines of a header. This takes milliseconds, while the
// quadratic implementation took minutes with this input. We don't check
// the timing, so a regression will show up as this test being very slow.
const n = 1_000_000
value := strings.Repeat("\r\n a", n)
message := "From: a@b\r\nLong:" + value + "\r\n\r\nbody"
headers, body, err := parseMessage(message)
if err != nil {
t.Fatalf("parseMessage error: %v", err)
}
if len(headers) != 2 {
t.Fatalf("wanted 2 headers, got %d", len(headers))
}
if headers[1].Name != "Long" {
t.Fatalf("unexpected header name: %q", headers[1].Name)
}
if headers[1].Value != value {
t.Errorf("unexpected value (len %d, want len %d)",
len(headers[1].Value), len(value))
}
if headers[1].Source != "Long:"+value {
t.Errorf("unexpected source (len %d, want len %d)",
len(headers[1].Source), len("Long:"+value))
}
if body != "body" {
t.Errorf("unexpected body: %q", body)
}
}
func TestHeadersFindAll(t *testing.T) {
hs := headers{
{Name: "From", Value: "a@b", Source: "From: a@b"},
{Name: "To", Value: "c@d", Source: "To: c@d"},
{Name: "Subject", Value: "test", Source: "Subject: test"},
{Name: "fROm", Value: "z@y", Source: "fROm: z@y"},
}
fromHs := hs.FindAll("froM")
expected := headers{
{Name: "From", Value: "a@b", Source: "From: a@b"},
{Name: "fROm", Value: "z@y", Source: "fROm: z@y"},
}
if diff := cmp.Diff(expected, fromHs); diff != "" {
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)
}
}