maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package dkim
 20
 21import (
 22	"bytes"
 23	"context"
 24	"os"
 25	"path/filepath"
 26	"reflect"
 27	"sort"
 28	"testing"
 29
 30	"github.com/emersion/go-message/textproto"
 31	"github.com/emersion/go-msgauth/dkim"
 32	"github.com/foxcpp/go-mockdns"
 33	"github.com/foxcpp/maddy/framework/buffer"
 34	"github.com/foxcpp/maddy/framework/config"
 35	"github.com/foxcpp/maddy/framework/module"
 36	"github.com/foxcpp/maddy/internal/testutils"
 37)
 38
 39func newTestModifier(t *testing.T, dir, keyAlgo string, domains []string) *Modifier {
 40	mod, err := New("", "test", nil, nil)
 41	if err != nil {
 42		t.Fatal(err)
 43	}
 44	m := mod.(*Modifier)
 45	m.log = testutils.Logger(t, m.Name())
 46
 47	err = m.Init(config.NewMap(nil, config.Node{
 48		Children: []config.Node{
 49			{
 50				Name: "domains",
 51				Args: domains,
 52			},
 53			{
 54				Name: "selector",
 55				Args: []string{"default"},
 56			},
 57			{
 58				Name: "key_path",
 59				Args: []string{filepath.Join(dir, "{domain}.key")},
 60			},
 61			{
 62				Name: "newkey_algo",
 63				Args: []string{keyAlgo},
 64			},
 65		},
 66	}))
 67	if err != nil {
 68		t.Fatal(err)
 69	}
 70
 71	return m
 72}
 73
 74func signTestMsg(t *testing.T, m *Modifier, envelopeFrom string) (textproto.Header, []byte) {
 75	t.Helper()
 76
 77	state, err := m.ModStateForMsg(context.Background(), &module.MsgMetadata{})
 78	if err != nil {
 79		t.Fatal(err)
 80	}
 81
 82	testHdr := textproto.Header{}
 83	testHdr.Add("From", "<hello@hello>")
 84	testHdr.Add("Subject", "heya")
 85	testHdr.Add("To", "<heya@heya>")
 86	body := []byte("hello there\r\n")
 87
 88	// modify.dkim expects RewriteSender to be called to get envelope sender
 89	//  (see module.Modifier docs)
 90
 91	// RewriteSender does not fail for modify.dkim. It just sets envelopeFrom.
 92	if _, err := state.RewriteSender(context.Background(), envelopeFrom); err != nil {
 93		panic(err)
 94	}
 95	err = state.RewriteBody(context.Background(), &testHdr, buffer.MemoryBuffer{Slice: body})
 96	if err != nil {
 97		t.Fatal(err)
 98	}
 99
100	return testHdr, body
101}
102
103func verifyTestMsg(t *testing.T, keysPath string, expectedDomains []string, hdr textproto.Header, body []byte) {
104	t.Helper()
105
106	domainsMap := make(map[string]bool)
107	zones := map[string]mockdns.Zone{}
108	for _, domain := range expectedDomains {
109		dnsRecord, err := os.ReadFile(filepath.Join(keysPath, domain+".dns"))
110		if err != nil {
111			t.Fatal(err)
112		}
113
114		t.Log("DNS record:", string(dnsRecord))
115		zones["default._domainkey."+domain+"."] = mockdns.Zone{TXT: []string{string(dnsRecord)}}
116		domainsMap[domain] = false
117	}
118
119	var fullBody bytes.Buffer
120	if err := textproto.WriteHeader(&fullBody, hdr); err != nil {
121		t.Fatal(err)
122	}
123	if _, err := fullBody.Write(body); err != nil {
124		t.Fatal(err)
125	}
126
127	resolver := &mockdns.Resolver{Zones: zones}
128	verifs, err := dkim.VerifyWithOptions(bytes.NewReader(fullBody.Bytes()), &dkim.VerifyOptions{
129		LookupTXT: func(domain string) ([]string, error) {
130			return resolver.LookupTXT(context.Background(), domain)
131		},
132	})
133	if err != nil {
134		t.Fatal(err)
135	}
136	for _, v := range verifs {
137		if v.Err != nil {
138			t.Errorf("Verification error for %s: %v", v.Domain, v.Err)
139		}
140		if _, ok := domainsMap[v.Domain]; !ok {
141			t.Errorf("Unexpected verification for domain %s", v.Domain)
142		}
143
144		domainsMap[v.Domain] = true
145	}
146	for domain, ok := range domainsMap {
147		if !ok {
148			t.Errorf("Missing verification for domain %s", domain)
149		}
150	}
151}
152
153func TestGenerateSignVerify(t *testing.T) {
154	// This test verifies whether a freshly generated key can be used for
155	// signing and verification.
156	//
157	// It is a kind of "integration" test for DKIM modifier, as it tests
158	// whether everything works correctly together.
159	//
160	// Additionally it also tests whether key selection works correctly.
161
162	test := func(domains []string, envelopeFrom string, expectDomain []string, keyAlgo string, headerCanon, bodyCanon dkim.Canonicalization, reload bool) {
163		t.Helper()
164
165		dir := t.TempDir()
166
167		m := newTestModifier(t, dir, keyAlgo, domains)
168		m.bodyCanon = bodyCanon
169		m.headerCanon = headerCanon
170		if reload {
171			m = newTestModifier(t, dir, keyAlgo, domains)
172		}
173
174		testHdr, body := signTestMsg(t, m, envelopeFrom)
175		verifyTestMsg(t, dir, expectDomain, testHdr, body)
176	}
177
178	for _, algo := range [2]string{"rsa2048", "ed25519"} {
179		for _, hdrCanon := range [2]dkim.Canonicalization{dkim.CanonicalizationSimple, dkim.CanonicalizationRelaxed} {
180			for _, bodyCanon := range [2]dkim.Canonicalization{dkim.CanonicalizationSimple, dkim.CanonicalizationRelaxed} {
181				test([]string{"maddy.test"}, "test@maddy.test", []string{"maddy.test"}, algo, hdrCanon, bodyCanon, false)
182				test([]string{"maddy.test"}, "test@maddy.test", []string{"maddy.test"}, algo, hdrCanon, bodyCanon, true)
183			}
184		}
185	}
186
187	// Key selection tests
188	test(
189		[]string{"maddy.test"}, // Generated keys.
190		"test@maddy.test",      // Envelope sender.
191		[]string{"maddy.test"}, // Expected signature domains.
192		"ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)
193	test(
194		[]string{"maddy.test"},
195		"test@unrelated.maddy.test",
196		[]string{},
197		"ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)
198	test(
199		[]string{"maddy.test", "related.maddy.test"},
200		"test@related.maddy.test",
201		[]string{"related.maddy.test"},
202		"ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)
203	test(
204		[]string{"fallback.maddy.test", "maddy.test"},
205		"postmaster",
206		[]string{"fallback.maddy.test"},
207		"ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)
208	test(
209		[]string{"fallback.maddy.test", "maddy.test"},
210		"",
211		[]string{"fallback.maddy.test"},
212		"ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)
213	test(
214		[]string{"another.maddy.test", "another.maddy.test", "maddy.test"},
215		"test@another.maddy.test",
216		[]string{"another.maddy.test"},
217		"ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)
218	test(
219		[]string{"another.maddy.test", "another.maddy.test", "maddy.test"},
220		"",
221		[]string{"another.maddy.test"},
222		"ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)
223}
224
225func TestFieldsToSign(t *testing.T) {
226	h := textproto.Header{}
227	h.Add("A", "1")
228	h.Add("c", "2")
229	h.Add("C", "3")
230	h.Add("a", "4")
231	h.Add("b", "5")
232	h.Add("unrelated", "6")
233
234	m := Modifier{
235		oversignHeader: []string{"A", "B"},
236		signHeader:     []string{"C"},
237	}
238	fields := m.fieldsToSign(&h)
239	sort.Strings(fields)
240	expected := []string{"A", "A", "A", "B", "B", "C", "C"}
241
242	if !reflect.DeepEqual(fields, expected) {
243		t.Errorf("incorrect set of fields to sign\nwant: %v\ngot:  %v", expected, fields)
244	}
245}