mirror of
https://blitiri.com.ar/repos/chasquid
synced 2026-10-29 15:02:30 +00:00
4b0b31a749
This patch adds two fuzzing tests to the dkim package: 1) Fuzz the sign-verify cycle, checking we can verify any message we sign. 2) Fuzz the message we verify, returning valid keys for any domain so it gets past the key fetching, and exercises a substantial part of the verification process. The first test already found two issues, which were fixed in the previous patches.
434 lines
11 KiB
Go
434 lines
11 KiB
Go
package dkim
|
||
|
||
import (
|
||
"context"
|
||
"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 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 {
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
|
||
func FuzzSignAndVerify(f *testing.F) {
|
||
f.Add(basicMessage)
|
||
f.Add("From: a@b\r\n\r\n")
|
||
f.Add("")
|
||
|
||
// Repeated headers with different values.
|
||
f.Add("From: a@b\r\nTo: c@d\r\n e@f\r\nTo: g@h\r\n\r\nbody\r\n")
|
||
|
||
// Non-ASCII header name (U+017F is a long s).
|
||
f.Add("From: a@b\r\nſubject: x\r\n\r\nbody\r\n")
|
||
|
||
// Messages from the file-based tests, which have more realistic headers,
|
||
// including existing DKIM signatures.
|
||
addTestdataCorpus(f)
|
||
|
||
// Use a fixed key, as generating one for each run would be too slow.
|
||
priv := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize))
|
||
pub := priv.Public().(ed25519.PublicKey)
|
||
ctx := WithLookupTXTFunc(context.Background(),
|
||
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,
|
||
}
|
||
|
||
f.Fuzz(func(t *testing.T, msg string) {
|
||
sig, err := s.Sign(ctx, msg)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
// Any message we can sign, we must be able to verify.
|
||
// Our signature is the first one, so it's always verified even if
|
||
// the message has other signatures.
|
||
res, err := VerifyMessage(ctx, addSig(sig, msg))
|
||
if err != nil {
|
||
t.Fatalf("VerifyMessage error: %v", err)
|
||
}
|
||
if len(res.Results) == 0 {
|
||
t.Fatalf("signature not found when verifying")
|
||
}
|
||
if r := res.Results[0]; r.State != SUCCESS {
|
||
t.Errorf("signature did not verify: %v / %v", r.State, r.Error)
|
||
}
|
||
})
|
||
}
|