1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2026-10-29 15:02:30 +00:00

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.
This commit is contained in:
Alberto Bertogli
2026-10-09 12:28:58 +01:00
parent 3b0836f811
commit b3b1f39d85
3 changed files with 57 additions and 1 deletions
+5 -1
View File
@@ -18,7 +18,11 @@ type headers []header
func (h headers) FindAll(name string) headers { func (h headers) FindAll(name string) headers {
hs := make(headers, 0) hs := make(headers, 0)
for _, header := range h { 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) hs = append(hs, header)
} }
} }
+11
View File
@@ -182,4 +182,15 @@ func TestHeadersFindAll(t *testing.T) {
t.Errorf("headers.Find() mismatch (-want +got):\n%s", 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)
}
} }
+41
View File
@@ -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 { func addSig(sig, message string) string {
return "DKIM-Signature: " + return "DKIM-Signature: " +
strings.ReplaceAll(sig, "\r\n", "\r\n\t") + strings.ReplaceAll(sig, "\r\n", "\r\n\t") +