mirror of
https://blitiri.com.ar/repos/chasquid
synced 2026-10-29 15:02:30 +00:00
b153955d5d
When verifying emails, for each entry in the h= tag, we would scan all headers, leading to quadratic behaviour. Similarly, when signing, for each instance of a signed header we would scan all headers. This could lead to unauthenticated DoS via work amplification. This patch fixes the problem by going through the headers only once, so behaviour is now linear, and also limiting the size of the h= tag for defense in depth. The limit is in line with what other implementations use, so it should not cause any interoperability problems. Thanks to John JEAN <john@wargan.com> for finding and reporting this problem. The detailed report can be found in https://johnjean.io/advisories/chasquid-unauthenticated-remote-dos/.
293 lines
7.4 KiB
Go
293 lines
7.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 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)
|
|
}
|
|
}
|
|
}
|