mirror of
https://blitiri.com.ar/repos/chasquid
synced 2026-10-29 15:02:30 +00:00
3b0836f811
When signing repeated headers, today we hash each instance of a header from top to bottom. However, RFC 6376 section 5.4.2 is clear they should be processed bottom to top, and this is also what our verifier (correctly) expects. The bug causes our signatures to be (wrongly) invalid for messages that have repeated signed headers with different values. Note that repeated headers that are not signed (e.g. Received:) are not problematic. This only affects headers we sign, like To:, From:, Subject:. This patch fixes the problem by adjusting the order in which Sign processes repeated headers. This bug was found by fuzzing the sign-verify cycle, which will be added in a later patch.
338 lines
8.4 KiB
Go
338 lines
8.4 KiB
Go
package dkim
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"errors"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
var basicMessage = toCRLF(
|
|
`Received: from client1.football.example.com [192.0.2.1]
|
|
by submitserver.example.com with SUBMISSION;
|
|
Fri, 11 Jul 2003 21:01:54 -0700 (PDT)
|
|
From: Joe SixPack <joe@football.example.com>
|
|
To: Suzie Q <suzie@shopping.example.net>
|
|
Subject: Is dinner ready?
|
|
Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
|
|
Message-ID: <20030712040037.46341.5F8J@football.example.com>
|
|
|
|
Hi.
|
|
|
|
We lost the game. Are you hungry yet?
|
|
|
|
Joe.
|
|
`)
|
|
|
|
func TestSignRSA(t *testing.T) {
|
|
ctx := t.Context()
|
|
ctx = WithTraceFunc(ctx, t.Logf)
|
|
|
|
// Generate a new key pair.
|
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("rsa.GenerateKey: %v", err)
|
|
}
|
|
pub, err := x509.MarshalPKIXPublicKey(priv.Public())
|
|
if err != nil {
|
|
t.Fatalf("MarshalPKIXPublicKey: %v", err)
|
|
}
|
|
|
|
ctx = WithLookupTXTFunc(ctx, makeLookupTXT(map[string][]string{
|
|
"test._domainkey.example.com": {
|
|
"v=DKIM1; p=" + base64.StdEncoding.EncodeToString(pub),
|
|
},
|
|
}))
|
|
|
|
s := &Signer{
|
|
Domain: "example.com",
|
|
Selector: "test",
|
|
Signer: priv,
|
|
}
|
|
|
|
sig, err := s.Sign(ctx, basicMessage)
|
|
if err != nil {
|
|
t.Fatalf("Sign: %v", err)
|
|
}
|
|
|
|
// Verify the signature.
|
|
res, err := VerifyMessage(ctx, addSig(sig, basicMessage))
|
|
if err != nil || res.Valid != 1 {
|
|
t.Errorf("VerifyMessage: wanted 1 valid / nil; got %v / %v", res, err)
|
|
}
|
|
|
|
// Compare the reproducible parts against a known-good header.
|
|
want := regexp.MustCompile(
|
|
"v=1; a=rsa-sha256; c=relaxed/relaxed;\r\n" +
|
|
"d=example.com; s=test; t=\\d+;\r\n" +
|
|
"h=from:subject:date:to:message-id:from:subject:date:to:cc:message-id;\r\n" +
|
|
"bh=[A-Za-z0-9+/]+=*;\r\n" +
|
|
"b=[A-Za-z0-9+/ \r\n]+=*;")
|
|
if !want.MatchString(sig) {
|
|
t.Errorf("Unexpected signature:")
|
|
t.Errorf(" Want: %q (regexp)", want)
|
|
t.Errorf(" Got: %q", sig)
|
|
}
|
|
}
|
|
|
|
func TestSignEd25519(t *testing.T) {
|
|
ctx := t.Context()
|
|
ctx = WithTraceFunc(ctx, t.Logf)
|
|
|
|
// Generate a new key pair.
|
|
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,
|
|
}
|
|
|
|
sig, err := s.Sign(ctx, basicMessage)
|
|
if err != nil {
|
|
t.Fatalf("Sign: %v", err)
|
|
}
|
|
|
|
// Verify the signature.
|
|
res, err := VerifyMessage(ctx, addSig(sig, basicMessage))
|
|
if err != nil || res.Valid != 1 {
|
|
t.Errorf("VerifyMessage: wanted 1 valid / nil; got %v / %v", res, err)
|
|
}
|
|
|
|
// Compare the reproducible parts against a known-good header.
|
|
want := regexp.MustCompile(
|
|
"v=1; a=ed25519-sha256; c=relaxed/relaxed;\r\n" +
|
|
"d=example.com; s=test; t=\\d+;\r\n" +
|
|
"h=from:subject:date:to:message-id:from:subject:date:to:cc:message-id;\r\n" +
|
|
"bh=[A-Za-z0-9+/]+=*;\r\n" +
|
|
"b=[A-Za-z0-9+/ \r\n]+=*;")
|
|
if !want.MatchString(sig) {
|
|
t.Errorf("Unexpected signature:")
|
|
t.Errorf(" Want: %q (regexp)", want)
|
|
t.Errorf(" Got: %q", sig)
|
|
}
|
|
}
|
|
|
|
func TestSignManyHeaders(t *testing.T) {
|
|
// Regression test for signing being quadratic on the number of instances
|
|
// 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 = 50_000
|
|
|
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("ed25519.GenerateKey: %v", err)
|
|
}
|
|
s := &Signer{
|
|
Domain: "example.com",
|
|
Selector: "test",
|
|
Signer: priv,
|
|
}
|
|
|
|
message := "From: from@example.com\r\n" +
|
|
strings.Repeat("To: to@example.com\r\n", n) +
|
|
"\r\nHi.\r\n"
|
|
|
|
sig, err := s.Sign(t.Context(), message)
|
|
if err != nil {
|
|
t.Fatalf("Sign: %v", err)
|
|
}
|
|
|
|
// All instances must be in h=, plus the extra one we always add.
|
|
hTag := regexp.MustCompile(`h=([^;]*);`).FindStringSubmatch(sig)
|
|
if hTag == nil {
|
|
t.Fatalf("h= not found in signature: %q", sig)
|
|
}
|
|
if got := strings.Count(hTag[1], "to"); got != n+1 {
|
|
t.Errorf("wanted %d instances of 'to' in h=, got %d", n+1, got)
|
|
}
|
|
}
|
|
|
|
func TestSignRepeatedHeaders(t *testing.T) {
|
|
// When a signed header appears multiple times (with different values),
|
|
// they must be signed from bottom to top, otherwise the signature will
|
|
// not verify.
|
|
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
|
|
To: to1@example.com
|
|
Subject: Subject 1
|
|
To: to2@example.com
|
|
Subject: Subject 2
|
|
To: to3@example.com
|
|
|
|
Hi.
|
|
`)
|
|
|
|
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") +
|
|
"\r\n" + message
|
|
}
|
|
|
|
func TestSignBadMessage(t *testing.T) {
|
|
s := &Signer{
|
|
Domain: "example.com",
|
|
Selector: "test",
|
|
}
|
|
_, err := s.Sign(t.Context(), "Bad message")
|
|
if err == nil {
|
|
t.Errorf("Sign: wanted error; got nil")
|
|
}
|
|
}
|
|
|
|
func TestSignBadAlgorithm(t *testing.T) {
|
|
s := &Signer{
|
|
Domain: "example.com",
|
|
Selector: "test",
|
|
}
|
|
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("ecdsa.GenerateKey: %v", err)
|
|
}
|
|
s.Signer = priv
|
|
|
|
_, err = s.Sign(t.Context(), basicMessage)
|
|
if !errors.Is(err, errUnsupportedKeyType) {
|
|
t.Errorf("Sign: wanted unsupported key type; got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBreakLongLines(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want string
|
|
}{
|
|
{"1234567890", "1234567890"},
|
|
{
|
|
"xxxxxxxx10xxxxxxxx20xxxxxxxx30xxxxxxxx40" +
|
|
"xxxxxxxx50xxxxxxxx60xxxxxxxx70",
|
|
"xxxxxxxx10xxxxxxxx20xxxxxxxx30xxxxxxxx40" +
|
|
"xxxxxxxx50xxxxxxxx60xxxxxxxx70",
|
|
},
|
|
{
|
|
"xxxxxxxx10xxxxxxxx20xxxxxxxx30xxxxxxxx40" +
|
|
"xxxxxxxx50xxxxxxxx60xxxxxxxx70123",
|
|
"xxxxxxxx10xxxxxxxx20xxxxxxxx30xxxxxxxx40" +
|
|
"xxxxxxxx50xxxxxxxx60xxxxxxxx70\r\n 123",
|
|
},
|
|
{
|
|
"xxxxxxxx10xxxxxxxx20xxxxxxxx30xxxxxxxx40" +
|
|
"xxxxxxxx50xxxxxxxx60xxxxxxxx70xxxxxxxx80" +
|
|
"xxxxxxxx90xxxxxxx100xxxxxxx110xxxxxxx120" +
|
|
"xxxxxxx130xxxxxxx140xxxxxxx150xxxxxxx160",
|
|
"xxxxxxxx10xxxxxxxx20xxxxxxxx30xxxxxxxx40" +
|
|
"xxxxxxxx50xxxxxxxx60xxxxxxxx70\r\n " +
|
|
"xxxxxxxx80xxxxxxxx90xxxxxxx100xxxxxxx110" +
|
|
"xxxxxxx120xxxxxxx130xxxxxxx140\r\n " +
|
|
"xxxxxxx150xxxxxxx160",
|
|
},
|
|
}
|
|
|
|
for i, c := range cases {
|
|
got := breakLongLines(c.in)
|
|
if got != c.want {
|
|
t.Errorf("%d: breakLongLines(%q):", i, c.in)
|
|
t.Errorf(" want %q", c.want)
|
|
t.Errorf(" got %q", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormatHeaders(t *testing.T) {
|
|
cases := []struct {
|
|
in []string
|
|
want string
|
|
}{
|
|
{[]string{"From"}, "from"},
|
|
{
|
|
[]string{"From", "Subject", "Date"},
|
|
"from:subject:date",
|
|
},
|
|
{
|
|
[]string{"from", "subject", "date", "to", "message-id",
|
|
"from", "subject", "date", "to", "cc", "in-reply-to",
|
|
"message-id"},
|
|
"from:subject:date:to:message-id:" +
|
|
"from:subject:date:to:cc:in-reply-to\r\n" +
|
|
" :message-id",
|
|
},
|
|
{
|
|
[]string{"from", "subject", "date", "to", "message-id",
|
|
"from", "subject", "date", "to", "cc", "xxxxxxxxxxxx70"},
|
|
"from:subject:date:to:message-id:" +
|
|
"from:subject:date:to:cc:xxxxxxxxxxxx70",
|
|
},
|
|
{
|
|
[]string{"from", "subject", "date", "to", "message-id",
|
|
"from", "subject", "date", "to", "cc", "xxxxxxxxxxxx701"},
|
|
"from:subject:date:to:message-id:from:subject:date:to:cc\r\n" +
|
|
" :xxxxxxxxxxxx701",
|
|
},
|
|
{
|
|
[]string{"from", "subject", "date", "to", "message-id",
|
|
"from", "subject", "date", "to", "cc", "xxxxxxxxxxxx70",
|
|
"1"},
|
|
"from:subject:date:to:message-id:" +
|
|
"from:subject:date:to:cc:xxxxxxxxxxxx70\r\n" +
|
|
" :1",
|
|
},
|
|
}
|
|
|
|
for i, c := range cases {
|
|
got := formatHeaders(c.in)
|
|
if got != c.want {
|
|
t.Errorf("%d: formatHeaders(%q):", i, c.in)
|
|
t.Errorf(" want %q", c.want)
|
|
t.Errorf(" got %q", got)
|
|
}
|
|
}
|
|
}
|