mirror of
https://blitiri.com.ar/repos/chasquid
synced 2026-10-28 15:02:28 +00:00
b3b1f39d85
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.
197 lines
5.1 KiB
Go
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)
|
|
}
|
|
|
|
}
|