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	"context"
 23	"errors"
 24	"net"
 25	"testing"
 26
 27	"github.com/emersion/go-msgauth/authres"
 28	"github.com/foxcpp/go-mockdns"
 29	"github.com/foxcpp/maddy/framework/buffer"
 30	"github.com/foxcpp/maddy/framework/config"
 31	"github.com/foxcpp/maddy/framework/exterrors"
 32	"github.com/foxcpp/maddy/framework/module"
 33	"github.com/foxcpp/maddy/internal/testutils"
 34)
 35
 36const unsignedMailString = `From: Joe SixPack <joe@football.example.com>
 37To: Suzie Q <suzie@shopping.example.net>
 38Subject: Is dinner ready?
 39Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
 40Message-ID: <20030712040037.46341.5F8J@football.example.com>
 41
 42Hi.
 43
 44We lost the game. Are you hungry yet?
 45
 46Joe.
 47`
 48
 49const dnsPublicKey = "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ" +
 50	"KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt" +
 51	"IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v" +
 52	"/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi" +
 53	"tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB"
 54
 55var testZones = map[string]mockdns.Zone{
 56	"brisbane._domainkey.example.com.": {
 57		TXT: []string{dnsPublicKey},
 58	},
 59}
 60
 61const verifiedMailString = `DKIM-Signature: v=1; a=rsa-sha256; s=brisbane; d=example.com;
 62      c=simple/simple; q=dns/txt; i=joe@football.example.com;
 63      h=Received : From : To : Subject : Date : Message-ID;
 64      bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
 65      b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB
 66      4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut
 67      KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV
 68      4bmp/YzhwvcubU4=;
 69Received: from client1.football.example.com  [192.0.2.1]
 70      by submitserver.example.com with SUBMISSION;
 71      Fri, 11 Jul 2003 21:01:54 -0700 (PDT)
 72From: Joe SixPack <joe@football.example.com>
 73To: Suzie Q <suzie@shopping.example.net>
 74Subject: Is dinner ready?
 75Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
 76Message-ID: <20030712040037.46341.5F8J@football.example.com>
 77
 78Hi.
 79
 80We lost the game. Are you hungry yet?
 81
 82Joe.
 83`
 84
 85func testCheck(t *testing.T, zones map[string]mockdns.Zone, cfg []config.Node) *Check {
 86	t.Helper()
 87	mod, err := New("check.dkim", "", nil, nil)
 88	if err != nil {
 89		t.Fatal(err)
 90	}
 91	check := mod.(*Check)
 92	check.resolver = &mockdns.Resolver{Zones: zones}
 93	check.log = testutils.Logger(t, mod.Name())
 94
 95	if err := check.Init(config.NewMap(nil, config.Node{Children: cfg})); err != nil {
 96		t.Fatal(err)
 97	}
 98
 99	return check
100}
101
102func TestDkimVerify_NoSig(t *testing.T) {
103	check := testCheck(t, nil, nil) // No zones since this test requires no lookups.
104
105	// Force certain reason so we can assert for it.
106	check.noSigAction.Reject = true
107	check.noSigAction.ReasonOverride = &exterrors.SMTPError{Code: 555}
108
109	ctx, cancel := context.WithCancel(context.Background())
110	defer cancel()
111
112	// The usual checking flow.
113	s, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{
114		ID: "test_unsigned",
115	})
116	if err != nil {
117		t.Fatal(err)
118	}
119	s.CheckConnection(ctx)
120	s.CheckSender(ctx, "joe@football.example.com")
121	s.CheckRcpt(ctx, "suzie@shopping.example.net")
122
123	hdr, buf := testutils.BodyFromStr(t, unsignedMailString)
124	result := s.CheckBody(ctx, hdr, buf)
125
126	if result.Reason == nil {
127		t.Fatal("No check fail reason set, auth. result:", authres.Format("", result.AuthResult))
128	}
129	if result.Reason.(*exterrors.SMTPError).Code != 555 {
130		t.Fatal("Different fail reason:", result.Reason)
131	}
132}
133
134func TestDkimVerify_InvalidSig(t *testing.T) {
135	check := testCheck(t, testZones, nil)
136
137	// Force certain reason so we can assert for it.
138	check.brokenSigAction.Reject = true
139	check.brokenSigAction.ReasonOverride = &exterrors.SMTPError{Code: 555}
140
141	ctx, cancel := context.WithCancel(context.Background())
142	defer cancel()
143
144	s, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{
145		ID: "test_unsigned",
146	})
147	if err != nil {
148		t.Fatal(err)
149	}
150
151	s.CheckConnection(ctx)
152	s.CheckSender(ctx, "joe@football.example.com")
153	s.CheckRcpt(ctx, "suzie@shopping.example.net")
154
155	hdr, buf := testutils.BodyFromStr(t, verifiedMailString)
156	// Mess up the signature.
157	hdr.Set("From", "nope")
158
159	result := s.CheckBody(ctx, hdr, buf)
160
161	if result.Reason == nil {
162		t.Fatal("No check fail reason set, auth. result:", authres.Format("", result.AuthResult))
163	}
164	if result.Reason.(*exterrors.SMTPError).Code != 555 {
165		t.Fatal("Different fail reason:", result.Reason)
166	}
167}
168
169func TestDkimVerify_ValidSig(t *testing.T) {
170	check := testCheck(t, testZones, nil)
171
172	ctx, cancel := context.WithCancel(context.Background())
173	defer cancel()
174
175	s, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{
176		ID: "test_unsigned",
177	})
178	if err != nil {
179		t.Fatal(err)
180	}
181
182	s.CheckConnection(ctx)
183	s.CheckSender(ctx, "joe@football.example.com")
184	s.CheckRcpt(ctx, "suzie@shopping.example.net")
185
186	hdr, buf := testutils.BodyFromStr(t, verifiedMailString)
187
188	result := s.CheckBody(ctx, hdr, buf)
189
190	if result.Reason != nil {
191		t.Log(authres.Format("", result.AuthResult))
192		t.Fatal("Check fail reason set, auth. result:", result.Reason, exterrors.Fields(result.Reason))
193	}
194}
195
196func TestDkimVerify_RequiredFields(t *testing.T) {
197	check := testCheck(t, testZones, []config.Node{
198		{
199			// Require field that is not covered by the signature.
200			Name: "required_fields",
201			Args: []string{"From", "X-Important"},
202		},
203	})
204
205	// Force certain reason so we can assert for it.
206	check.brokenSigAction.Reject = true
207	check.brokenSigAction.ReasonOverride = &exterrors.SMTPError{Code: 555}
208
209	ctx, cancel := context.WithCancel(context.Background())
210	defer cancel()
211
212	s, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{
213		ID: "test_unsigned",
214	})
215	if err != nil {
216		t.Fatal(err)
217	}
218
219	s.CheckConnection(ctx)
220	s.CheckSender(ctx, "joe@football.example.com")
221	s.CheckRcpt(ctx, "suzie@shopping.example.net")
222
223	hdr, buf := testutils.BodyFromStr(t, verifiedMailString)
224
225	result := s.CheckBody(ctx, hdr, buf)
226
227	if result.Reason == nil {
228		t.Fatal("No check fail reason set, auth. result:", authres.Format("", result.AuthResult))
229	}
230	if result.Reason.(*exterrors.SMTPError).Code != 555 {
231		t.Fatal("Different fail reason:", result.Reason)
232	}
233}
234
235func TestDkimVerify_BufferOpenFail(t *testing.T) {
236	check := testCheck(t, testZones, nil)
237
238	ctx, cancel := context.WithCancel(context.Background())
239	defer cancel()
240
241	s, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{
242		ID: "test_unsigned",
243	})
244	if err != nil {
245		t.Fatal(err)
246	}
247
248	s.CheckConnection(ctx)
249	s.CheckSender(ctx, "joe@football.example.com")
250	s.CheckRcpt(ctx, "suzie@shopping.example.net")
251
252	var buf buffer.Buffer
253	hdr, buf := testutils.BodyFromStr(t, verifiedMailString)
254	buf = testutils.FailingBuffer{Blob: buf.(buffer.MemoryBuffer).Slice, OpenError: errors.New("No!")}
255
256	result := s.CheckBody(ctx, hdr, buf)
257	t.Log("auth. result:", authres.Format("", result.AuthResult))
258
259	if result.Reason == nil {
260		t.Fatal("No check fail reason set, auth. result:", authres.Format("", result.AuthResult))
261	}
262}
263
264func TestDkimVerify_FailClosed(t *testing.T) {
265	zones := map[string]mockdns.Zone{
266		"brisbane._domainkey.example.com.": {
267			Err: &net.DNSError{
268				Err:         "DNS server is not having a great time",
269				IsTemporary: true,
270				IsTimeout:   true,
271			},
272		},
273	}
274	check := testCheck(t, zones, []config.Node{
275		{
276			Name: "fail_open",
277			Args: []string{"false"},
278		},
279	})
280
281	ctx, cancel := context.WithCancel(context.Background())
282	defer cancel()
283
284	s, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{
285		ID: "test_unsigned",
286	})
287	if err != nil {
288		t.Fatal(err)
289	}
290
291	s.CheckConnection(ctx)
292	s.CheckSender(ctx, "joe@football.example.com")
293	s.CheckRcpt(ctx, "suzie@shopping.example.net")
294
295	hdr, buf := testutils.BodyFromStr(t, verifiedMailString)
296
297	result := s.CheckBody(ctx, hdr, buf)
298	t.Log("auth. result:", authres.Format("", result.AuthResult))
299
300	if result.Reason == nil {
301		t.Fatal("No check fail reason set, auth. result:", authres.Format("", result.AuthResult))
302	}
303	if !result.Reject {
304		t.Fatal("No reject requested")
305	}
306	if !exterrors.IsTemporary(result.Reason) {
307		t.Fatal("Fail reason is not marked as temporary:", result.Reason)
308	}
309}
310
311func TestDkimVerify_FailOpen(t *testing.T) {
312	zones := map[string]mockdns.Zone{
313		"brisbane._domainkey.example.com.": {
314			Err: &net.DNSError{
315				Err:         "DNS server is not having a great time",
316				IsTemporary: true,
317				IsTimeout:   true,
318			},
319		},
320	}
321	check := testCheck(t, zones, []config.Node{
322		{
323			Name: "fail_open",
324			Args: []string{"true"},
325		},
326	})
327
328	ctx, cancel := context.WithCancel(context.Background())
329	defer cancel()
330
331	s, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{
332		ID: "test_unsigned",
333	})
334	if err != nil {
335		t.Fatal(err)
336	}
337
338	s.CheckConnection(ctx)
339	s.CheckSender(ctx, "joe@football.example.com")
340	s.CheckRcpt(ctx, "suzie@shopping.example.net")
341
342	hdr, buf := testutils.BodyFromStr(t, verifiedMailString)
343
344	result := s.CheckBody(ctx, hdr, buf)
345
346	t.Log("auth. result:", authres.Format("", result.AuthResult))
347	if result.Reason == nil {
348		t.Fatal("No check fail reason set, auth. result:", authres.Format("", result.AuthResult))
349	}
350	if result.Reject {
351		t.Fatal("Reject requested")
352	}
353	if exterrors.IsTemporary(result.Reason) {
354		t.Fatal("Fail reason is not marked as temporary:", result.Reason)
355	}
356
357	if len(result.AuthResult) != 1 {
358		t.Fatal("Wrong amount of auth. result fields:", len(result.AuthResult))
359	}
360	resVal := result.AuthResult[0].(*authres.DKIMResult).Value
361	if resVal != authres.ResultTempError {
362		t.Fatal("Result is not temp. error:", resVal)
363	}
364}