1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package dkim2021import (22 "context"23 "errors"24 "net"25 "testing"2627 "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)3536const 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>4142Hi.4344We lost the game. Are you hungry yet?4546Joe.47`4849const dnsPublicKey = "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ" +50 "KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt" +51 "IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v" +52 "/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi" +53 "tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB"5455var testZones = map[string]mockdns.Zone{56 "brisbane._domainkey.example.com.": {57 TXT: []string{dnsPublicKey},58 },59}6061const 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=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB66 4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut67 KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV68 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>7778Hi.7980We lost the game. Are you hungry yet?8182Joe.83`8485func 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())9495 if err := check.Init(config.NewMap(nil, config.Node{Children: cfg})); err != nil {96 t.Fatal(err)97 }9899 return check100}101102func TestDkimVerify_NoSig(t *testing.T) {103 check := testCheck(t, nil, nil) // No zones since this test requires no lookups.104105 // Force certain reason so we can assert for it.106 check.noSigAction.Reject = true107 check.noSigAction.ReasonOverride = &exterrors.SMTPError{Code: 555}108109 ctx, cancel := context.WithCancel(context.Background())110 defer cancel()111112 // 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")122123 hdr, buf := testutils.BodyFromStr(t, unsignedMailString)124 result := s.CheckBody(ctx, hdr, buf)125126 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}133134func TestDkimVerify_InvalidSig(t *testing.T) {135 check := testCheck(t, testZones, nil)136137 // Force certain reason so we can assert for it.138 check.brokenSigAction.Reject = true139 check.brokenSigAction.ReasonOverride = &exterrors.SMTPError{Code: 555}140141 ctx, cancel := context.WithCancel(context.Background())142 defer cancel()143144 s, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{145 ID: "test_unsigned",146 })147 if err != nil {148 t.Fatal(err)149 }150151 s.CheckConnection(ctx)152 s.CheckSender(ctx, "joe@football.example.com")153 s.CheckRcpt(ctx, "suzie@shopping.example.net")154155 hdr, buf := testutils.BodyFromStr(t, verifiedMailString)156 // Mess up the signature.157 hdr.Set("From", "nope")158159 result := s.CheckBody(ctx, hdr, buf)160161 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}168169func TestDkimVerify_ValidSig(t *testing.T) {170 check := testCheck(t, testZones, nil)171172 ctx, cancel := context.WithCancel(context.Background())173 defer cancel()174175 s, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{176 ID: "test_unsigned",177 })178 if err != nil {179 t.Fatal(err)180 }181182 s.CheckConnection(ctx)183 s.CheckSender(ctx, "joe@football.example.com")184 s.CheckRcpt(ctx, "suzie@shopping.example.net")185186 hdr, buf := testutils.BodyFromStr(t, verifiedMailString)187188 result := s.CheckBody(ctx, hdr, buf)189190 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}195196func 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 })204205 // Force certain reason so we can assert for it.206 check.brokenSigAction.Reject = true207 check.brokenSigAction.ReasonOverride = &exterrors.SMTPError{Code: 555}208209 ctx, cancel := context.WithCancel(context.Background())210 defer cancel()211212 s, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{213 ID: "test_unsigned",214 })215 if err != nil {216 t.Fatal(err)217 }218219 s.CheckConnection(ctx)220 s.CheckSender(ctx, "joe@football.example.com")221 s.CheckRcpt(ctx, "suzie@shopping.example.net")222223 hdr, buf := testutils.BodyFromStr(t, verifiedMailString)224225 result := s.CheckBody(ctx, hdr, buf)226227 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}234235func TestDkimVerify_BufferOpenFail(t *testing.T) {236 check := testCheck(t, testZones, nil)237238 ctx, cancel := context.WithCancel(context.Background())239 defer cancel()240241 s, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{242 ID: "test_unsigned",243 })244 if err != nil {245 t.Fatal(err)246 }247248 s.CheckConnection(ctx)249 s.CheckSender(ctx, "joe@football.example.com")250 s.CheckRcpt(ctx, "suzie@shopping.example.net")251252 var buf buffer.Buffer253 hdr, buf := testutils.BodyFromStr(t, verifiedMailString)254 buf = testutils.FailingBuffer{Blob: buf.(buffer.MemoryBuffer).Slice, OpenError: errors.New("No!")}255256 result := s.CheckBody(ctx, hdr, buf)257 t.Log("auth. result:", authres.Format("", result.AuthResult))258259 if result.Reason == nil {260 t.Fatal("No check fail reason set, auth. result:", authres.Format("", result.AuthResult))261 }262}263264func 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 })280281 ctx, cancel := context.WithCancel(context.Background())282 defer cancel()283284 s, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{285 ID: "test_unsigned",286 })287 if err != nil {288 t.Fatal(err)289 }290291 s.CheckConnection(ctx)292 s.CheckSender(ctx, "joe@football.example.com")293 s.CheckRcpt(ctx, "suzie@shopping.example.net")294295 hdr, buf := testutils.BodyFromStr(t, verifiedMailString)296297 result := s.CheckBody(ctx, hdr, buf)298 t.Log("auth. result:", authres.Format("", result.AuthResult))299300 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}310311func 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 })327328 ctx, cancel := context.WithCancel(context.Background())329 defer cancel()330331 s, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{332 ID: "test_unsigned",333 })334 if err != nil {335 t.Fatal(err)336 }337338 s.CheckConnection(ctx)339 s.CheckSender(ctx, "joe@football.example.com")340 s.CheckRcpt(ctx, "suzie@shopping.example.net")341342 hdr, buf := testutils.BodyFromStr(t, verifiedMailString)343344 result := s.CheckBody(ctx, hdr, buf)345346 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 }356357 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).Value361 if resVal != authres.ResultTempError {362 t.Fatal("Result is not temp. error:", resVal)363 }364}