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 "bytes"23 "context"24 "os"25 "path/filepath"26 "reflect"27 "sort"28 "testing"2930 "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)3839func 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())4647 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 }7071 return m72}7374func signTestMsg(t *testing.T, m *Modifier, envelopeFrom string) (textproto.Header, []byte) {75 t.Helper()7677 state, err := m.ModStateForMsg(context.Background(), &module.MsgMetadata{})78 if err != nil {79 t.Fatal(err)80 }8182 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")8788 // modify.dkim expects RewriteSender to be called to get envelope sender89 // (see module.Modifier docs)9091 // 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 }99100 return testHdr, body101}102103func verifyTestMsg(t *testing.T, keysPath string, expectedDomains []string, hdr textproto.Header, body []byte) {104 t.Helper()105106 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 }113114 t.Log("DNS record:", string(dnsRecord))115 zones["default._domainkey."+domain+"."] = mockdns.Zone{TXT: []string{string(dnsRecord)}}116 domainsMap[domain] = false117 }118119 var fullBody bytes.Buffer120 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 }126127 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 }143144 domainsMap[v.Domain] = true145 }146 for domain, ok := range domainsMap {147 if !ok {148 t.Errorf("Missing verification for domain %s", domain)149 }150 }151}152153func TestGenerateSignVerify(t *testing.T) {154 // This test verifies whether a freshly generated key can be used for155 // signing and verification.156 //157 // It is a kind of "integration" test for DKIM modifier, as it tests158 // whether everything works correctly together.159 //160 // Additionally it also tests whether key selection works correctly.161162 test := func(domains []string, envelopeFrom string, expectDomain []string, keyAlgo string, headerCanon, bodyCanon dkim.Canonicalization, reload bool) {163 t.Helper()164165 dir := t.TempDir()166167 m := newTestModifier(t, dir, keyAlgo, domains)168 m.bodyCanon = bodyCanon169 m.headerCanon = headerCanon170 if reload {171 m = newTestModifier(t, dir, keyAlgo, domains)172 }173174 testHdr, body := signTestMsg(t, m, envelopeFrom)175 verifyTestMsg(t, dir, expectDomain, testHdr, body)176 }177178 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 }186187 // Key selection tests188 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}224225func 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")233234 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"}241242 if !reflect.DeepEqual(fields, expected) {243 t.Errorf("incorrect set of fields to sign\nwant: %v\ngot: %v", expected, fields)244 }245}