diff --git a/docs/man/maddy-filters.5.scd b/docs/man/maddy-filters.5.scd index 07d8b80..4fa7a0a 100644 --- a/docs/man/maddy-filters.5.scd +++ b/docs/man/maddy-filters.5.scd @@ -395,7 +395,7 @@ protocol (RFC 6376). ``` sign_dkim { debug no - domain example.org + domains example.org example.com selector default key_path dkim-keys/{domain}-{selector}.key oversign_fields ... @@ -410,7 +410,7 @@ sign_dkim { ## Arguments -domain and selector can be specified in arguments, so actual sign_dkim use can +domains and selector can be specified in arguments, so actual sign_dkim use can be shortened to the following: ``` modify { @@ -425,14 +425,20 @@ modify { Enable verbose logging for sign_dkim. -*Syntax*: domain _string_ ++ +*Syntax*: domains _string list_ ++ *Default*: not specified *REQUIRED.* -ADministrative Management Domain (ADMD) taking responsibility for signed -messages. Should be specified either as a directive or as an -argument. +ADministrative Management Domains (ADMDs) taking responsibility for messages. + +A key will be generated or read for each domain specified here, the key to use +for each message will be selected based on the SMTP envelope sender. Exception +for that is that for domain-less postmaster address and null address, the +key for the first domain will be used. If domain in envelope sender +does not match any of loaded keys, message will not be signed. + +Should be specified either as a directive or as an argument. *Syntax*: selector _string_ ++ *Default*: not specified diff --git a/docs/tutorials/multiple-domains.md b/docs/tutorials/multiple-domains.md index 46873b3..0e3c830 100644 --- a/docs/tutorials/multiple-domains.md +++ b/docs/tutorials/multiple-domains.md @@ -18,48 +18,6 @@ both domains in the name, send and receive messages and so on. Do not forget to configure corresponding SPF, DMARC and MTA-STS records as was recommended in the [introduction tutorial](setting-up.md). -### DKIM - -However, one thing needs special attention since at the moment maddy lacks the -ability to automatically select the corresponding DKIM key. Without additional -changes with the above configuration it will sign all messages using the key -for the primary domain. So you should configure corresponding mappings to make -it use the proper key depending on the sender domain. - -To do so, open your configuration and look for the `submission` endpoint block. -Then take a look at `default_destination` directive that is responsible for -handling deliveries to non-local addresses there. - -You will notice it referes to the `local_modifiers` block which uses `sign_dkim -$(primary_domain) default`. It is kinda obvious what is happening here. - -First, remove the `deliver_to &remote_queue` line from here and replace it with -the following: -``` -reroute { - source example.com { - modify { sign_dkim example.com default } - deliver_to &remote_queue - } - source example.org { - modify { sign_dkim example.org default } - deliver_to &remote_queue - } - default_source { - reject 501 5.1.8 "Non-local sender domain" - } -} -``` -Replace example.com and example.org with your domains. Add more `source` -blocks if you need to handle more domains. - -This whole block tells maddy to take a look at the sender domain after deciding -that the message should be sent to the Internet and apply the corresponding set -of modifiers. Each set of modifiers consequently contains the `sign_dkim` -module reference that is responsible for DKIM signature creation using -domain-specific key. After that the message ends up in the outbound queue as -usual. - ## Single account namespace Lets say you want to handle messages for domains example.org and example.com @@ -95,4 +53,3 @@ Note, however, no account credentials aliasing is done. Users should always use the account name with the primary domain to access IMAP mailboxes. **Note 1**: All domains should still be listed in the `$(local_domains)` macro. -**Note 2**: Section about DKIM key selection still applies. diff --git a/internal/modify/dkim/dkim.go b/internal/modify/dkim/dkim.go index 95c246d..1a1c3a0 100644 --- a/internal/modify/dkim/dkim.go +++ b/internal/modify/dkim/dkim.go @@ -4,8 +4,8 @@ import ( "context" "crypto" "errors" + "fmt" "io" - "net/mail" "path/filepath" "runtime/trace" "strings" @@ -22,7 +22,6 @@ import ( "github.com/foxcpp/maddy/internal/module" "github.com/foxcpp/maddy/internal/target" "golang.org/x/net/idna" - "golang.org/x/text/unicode/norm" ) const Day = 86400 * time.Second @@ -79,9 +78,9 @@ var ( type Modifier struct { instName string - domain string + domains []string selector string - signer crypto.Signer + signers map[string]crypto.Signer oversignHeader []string signHeader []string headerCanon dkim.Canonicalization @@ -97,20 +96,19 @@ type Modifier struct { func New(_, instName string, _, inlineArgs []string) (module.Module, error) { m := &Modifier{ instName: instName, + signers: map[string]crypto.Signer{}, log: log.Logger{Name: "sign_dkim"}, } - switch len(inlineArgs) { - case 2: - m.domain = inlineArgs[0] - m.selector = inlineArgs[1] - case 0: - // whatever - case 1: - fallthrough - default: - return nil, errors.New("sign_dkim: wrong amount of inline arguments") + if len(inlineArgs) == 0 { + return m, nil } + if len(inlineArgs) == 1 { + return nil, errors.New("sign_dkim: at least two arguments required") + } + + m.domains = inlineArgs[0 : len(inlineArgs)-1] + m.selector = inlineArgs[len(inlineArgs)-1] return m, nil } @@ -132,7 +130,7 @@ func (m *Modifier) Init(cfg *config.Map) error { ) cfg.Bool("debug", true, false, &m.log.Debug) - cfg.String("domain", false, false, m.domain, &m.domain) + cfg.StringList("domains", false, false, m.domains, &m.domains) cfg.String("selector", false, false, m.selector, &m.selector) cfg.String("key_path", false, false, "dkim_keys/{domain}_{selector}.key", &keyPathTemplate) cfg.StringList("oversign_fields", false, false, oversignDefault, &m.oversignHeader) @@ -156,8 +154,8 @@ func (m *Modifier) Init(cfg *config.Map) error { return err } - if m.domain == "" { - return errors.New("sign_domain: domain is not specified") + if len(m.domains) == 0 { + return errors.New("sign_domain: at least one domain is needed") } if m.selector == "" { return errors.New("sign_domain: selector is not specified") @@ -176,25 +174,35 @@ func (m *Modifier) Init(cfg *config.Map) error { panic("sign_dkim.Init: Hash function allowed by config matcher but not present in hashFuncs") } - keyValues := strings.NewReplacer("{domain}", m.domain, "{selector}", m.selector) - keyPath := keyValues.Replace(keyPathTemplate) - - signer, newKey, err := m.loadOrGenerateKey(keyPath, newKeyAlgo) - if err != nil { - return err - } - - if newKey { - dnsPath := keyPath + ".dns" - if filepath.Ext(keyPath) == ".key" { - dnsPath = keyPath[:len(keyPath)-4] + ".dns" + for _, domain := range m.domains { + if _, err := idna.ToASCII(domain); err != nil { + m.log.Printf("warning: unable to convert domain %s to A-labels form, non-EAI messages will not be signed: %v", domain, err) } - m.log.Printf("generated a new %s keypair, private key is in %s, TXT record with public key is in %s,\n"+ - "put its contents into TXT record for %s._domainkey.%s to make signing and verification work", - newKeyAlgo, keyPath, dnsPath, m.selector, m.domain) - } - m.signer = signer + keyValues := strings.NewReplacer("{domain}", domain, "{selector}", m.selector) + keyPath := keyValues.Replace(keyPathTemplate) + + signer, newKey, err := m.loadOrGenerateKey(keyPath, newKeyAlgo) + if err != nil { + return err + } + + if newKey { + dnsPath := keyPath + ".dns" + if filepath.Ext(keyPath) == ".key" { + dnsPath = keyPath[:len(keyPath)-4] + ".dns" + } + m.log.Printf("generated a new %s keypair, private key is in %s, TXT record with public key is in %s,\n"+ + "put its contents into TXT record for %s._domainkey.%s to make signing and verification work", + newKeyAlgo, keyPath, dnsPath, m.selector, domain) + } + + normDomain, err := dns.ForLookup(domain) + if err != nil { + return fmt.Errorf("sign_skim: unable to normalize domain %s: %w", domain, err) + } + m.signers[normDomain] = signer + } return nil } @@ -235,101 +243,20 @@ func (m *Modifier) fieldsToSign(h *textproto.Header) []string { type state struct { m *Modifier meta *module.MsgMetadata + from string log log.Logger } func (m *Modifier) ModStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.ModifierState, error) { - return state{ + return &state{ m: m, meta: msgMeta, log: target.DeliveryLogger(m.log, msgMeta), }, nil } -func (m *Modifier) shouldSign(eai bool, msgId string, h *textproto.Header, mailFrom string, authName string) (string, bool) { - if _, off := m.senderMatch["off"]; off { - if !eai { - aDomain, err := idna.ToASCII(m.domain) - if err != nil { - m.log.Msg("not signing, cannot convert key domain domain into A-labels", - "from_addr", m.domain, "msg_id", msgId) - return "", false - } - - return "@" + aDomain, true - } - return "@" + m.domain, true - } - - fromVal := h.Get("From") - if fromVal == "" { - m.log.Msg("not signing, empty From", "msg_id", msgId) - return "", false - } - fromAddrs, err := mail.ParseAddressList(fromVal) - if err != nil { - m.log.Msg("not signing, malformed From field", "err", err, "msg_id", msgId) - return "", false - } - if len(fromAddrs) != 1 && !m.multipleFromOk { - m.log.Msg("not signing, multiple addresses in From", "msg_id", msgId) - return "", false - } - - fromAddr := fromAddrs[0].Address - fromUser, fromDomain, err := address.Split(fromAddr) - if err != nil { - m.log.Msg("not signing, malformed address in From", - "err", err, "from_addr", fromAddr, "msg_id", msgId) - return "", false - } - - if !dns.Equal(fromDomain, m.domain) { - m.log.Msg("not signing, From domain is not key domain", - "from_domain", fromDomain, "key_domain", m.domain, "msg_id", msgId) - return "", false - } - - if _, do := m.senderMatch["envelope"]; do && !address.Equal(fromAddr, mailFrom) { - m.log.Msg("not signing, From address is not envelope address", - "from_addr", fromAddr, "envelope", mailFrom, "msg_id", msgId) - return "", false - } - - if _, do := m.senderMatch["auth"]; do { - compareWith := norm.NFC.String(fromUser) - authName := norm.NFC.String(authName) - if strings.Contains(authName, "@") { - compareWith, _ = address.ForLookup(fromAddr) - } - if !strings.EqualFold(compareWith, authName) { - m.log.Msg("not signing, From address is not authenticated identity", - "from_addr", fromAddr, "auth_id", authName, "msg_id", msgId) - return "", false - } - } - - // Don't include non-ASCII in the identifier if message is - // non-EAI. - if !eai { - aDomain, err := idna.ToASCII(fromDomain) - if err != nil { - m.log.Msg("not signing, cannot convert From domain into A-labels", - "from_addr", fromAddr, "msg_id", msgId) - return "", false - } - - if !address.IsASCII(fromUser) { - return "@" + aDomain, true - } - - return fromUser + "@" + aDomain, true - } - - return fromAddr, true -} - -func (s state) RewriteSender(ctx context.Context, mailFrom string) (string, error) { +func (s *state) RewriteSender(ctx context.Context, mailFrom string) (string, error) { + s.from = mailFrom return mailFrom, nil } @@ -337,42 +264,54 @@ func (s state) RewriteRcpt(ctx context.Context, rcptTo string) (string, error) { return rcptTo, nil } -func (s state) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error { +func (s *state) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error { defer trace.StartRegion(ctx, "sign_dkim/RewriteBody").End() - var authUser string - if s.meta.Conn != nil { - authUser = s.meta.Conn.AuthUser + var domain string + if s.from != "" { + var err error + _, domain, err = address.Split(s.from) + if err != nil { + return err + } } + // Use first key for null return path (<>) and postmaster () + if domain == "" { + domain = s.m.domains[0] + } + selector := s.m.selector - id, ok := s.m.shouldSign(s.meta.SMTPOpts.UTF8, s.meta.ID, h, s.meta.OriginalFrom, authUser) - if !ok { + normDomain, err := dns.ForLookup(domain) + if err != nil { + s.log.Error("unable to normalize domain from envelope sender", err, "domain", domain) + return nil + } + keySigner := s.m.signers[normDomain] + if keySigner == nil { + s.log.Msg("no key for domain", "domain", normDomain) return nil } - domain := s.m.domain - selector := s.m.selector - - // If the message is non-EAI, we are not alloed to use domains in U-labels, + // If the message is non-EAI, we are not allowed to use domains in U-labels, // attempt to convert. if !s.meta.SMTPOpts.UTF8 { var err error - domain, err = idna.ToASCII(s.m.domain) + domain, err = idna.ToASCII(domain) if err != nil { - return exterrors.WithFields(err, map[string]interface{}{"modifier": "sign_dkim"}) + return nil } - selector, err = idna.ToASCII(s.m.selector) + selector, err = idna.ToASCII(selector) if err != nil { - return exterrors.WithFields(err, map[string]interface{}{"modifier": "sign_dkim"}) + return nil } } opts := dkim.SignOptions{ Domain: domain, Selector: selector, - Identifier: id, - Signer: s.m.signer, + Identifier: "@" + domain, + Signer: keySigner, Hash: s.m.hash, HeaderCanonicalization: s.m.headerCanon, BodyCanonicalization: s.m.bodyCanon, @@ -405,7 +344,7 @@ func (s state) RewriteBody(ctx context.Context, h *textproto.Header, body buffer h.AddRaw([]byte(signer.Signature())) - s.m.log.DebugMsg("signed", "identifier", id) + s.m.log.DebugMsg("signed", "domain", domain) return nil } diff --git a/internal/modify/dkim/dkim_test.go b/internal/modify/dkim/dkim_test.go index b011210..47dc888 100644 --- a/internal/modify/dkim/dkim_test.go +++ b/internal/modify/dkim/dkim_test.go @@ -9,7 +9,6 @@ import ( "path/filepath" "reflect" "sort" - "strconv" "testing" "github.com/emersion/go-message/textproto" @@ -21,7 +20,7 @@ import ( "github.com/foxcpp/maddy/internal/testutils" ) -func newTestModifier(t *testing.T, dir, keyAlgo string) *Modifier { +func newTestModifier(t *testing.T, dir, keyAlgo string, domains []string) *Modifier { mod, err := New("", "test", nil, nil) if err != nil { t.Fatal(err) @@ -32,8 +31,8 @@ func newTestModifier(t *testing.T, dir, keyAlgo string) *Modifier { err = m.Init(config.NewMap(nil, config.Node{ Children: []config.Node{ { - Name: "domain", - Args: []string{"maddy.test"}, + Name: "domains", + Args: domains, }, { Name: "selector", @@ -41,7 +40,7 @@ func newTestModifier(t *testing.T, dir, keyAlgo string) *Modifier { }, { Name: "key_path", - Args: []string{filepath.Join(dir, "testkey.key")}, + Args: []string{filepath.Join(dir, "{domain}.key")}, }, { Name: "require_sender_match", @@ -60,7 +59,9 @@ func newTestModifier(t *testing.T, dir, keyAlgo string) *Modifier { return m } -func signTestMsg(t *testing.T, m *Modifier) (textproto.Header, []byte) { +func signTestMsg(t *testing.T, m *Modifier, envelopeFrom string) (textproto.Header, []byte) { + t.Helper() + state, err := m.ModStateForMsg(context.Background(), &module.MsgMetadata{}) if err != nil { t.Fatal(err) @@ -72,6 +73,9 @@ func signTestMsg(t *testing.T, m *Modifier) (textproto.Header, []byte) { testHdr.Add("To", "") body := []byte("hello there\r\n") + // sign_dkim expects RewriteSender to be called to get envelope sender + // (see module.Modifier docs) + state.RewriteSender(context.Background(), envelopeFrom) err = state.RewriteBody(context.Background(), &testHdr, buffer.MemoryBuffer{Slice: body}) if err != nil { t.Fatal(err) @@ -80,20 +84,22 @@ func signTestMsg(t *testing.T, m *Modifier) (textproto.Header, []byte) { return testHdr, body } -func verifyTestMsg(t *testing.T, dnsPath string, hdr textproto.Header, body []byte) { - dnsRecord, err := ioutil.ReadFile(dnsPath) - if err != nil { - t.Fatal(err) +func verifyTestMsg(t *testing.T, keysPath string, expectedDomains []string, hdr textproto.Header, body []byte) { + t.Helper() + + domainsMap := make(map[string]bool) + zones := map[string]mockdns.Zone{} + for _, domain := range expectedDomains { + dnsRecord, err := ioutil.ReadFile(filepath.Join(keysPath, domain+".dns")) + if err != nil { + t.Fatal(err) + } + + t.Log("DNS record:", string(dnsRecord)) + zones["default._domainkey."+domain+"."] = mockdns.Zone{TXT: []string{string(dnsRecord)}} + domainsMap[domain] = false } - t.Log("DNS record:", string(dnsRecord)) - - zones := map[string]mockdns.Zone{ - "default._domainkey.maddy.test.": { - TXT: []string{string(dnsRecord)}, - }, - } - t.Log(string(dnsRecord)) // dkim.Verify does not allow to override its lookup routine, so we have to // hjack the global resolver object. srv, err := mockdns.NewServer(zones) @@ -112,15 +118,24 @@ func verifyTestMsg(t *testing.T, dnsPath string, hdr textproto.Header, body []by t.Fatal(err) } - v, err := dkim.Verify(bytes.NewReader(fullBody.Bytes())) + verifs, err := dkim.Verify(bytes.NewReader(fullBody.Bytes())) if err != nil { t.Fatal(err) } - if len(v) != 1 { - t.Fatal("Expected exactly one verification") + for _, v := range verifs { + if v.Err != nil { + t.Errorf("Verification error for %s: %v", v.Domain, v.Err) + } + if _, ok := domainsMap[v.Domain]; !ok { + t.Errorf("Unexpected verification for domain %s", v.Domain) + } + + domainsMap[v.Domain] = true } - if v[0].Err != nil { - t.Fatal("Verification error:", v[0].Err) + for domain, ok := range domainsMap { + if !ok { + t.Errorf("Missing verification for domain %s", domain) + } } } @@ -130,8 +145,10 @@ func TestGenerateSignVerify(t *testing.T) { // // It is a kind of "integration" test for DKIM modifier, as it tests // whether everything works correctly together. + // + // Additionally it also tests whether key selection works correctly. - test := func(keyAlgo string, headerCanon, bodyCanon dkim.Canonicalization, reload bool) { + test := func(domains []string, envelopeFrom string, expectDomain []string, keyAlgo string, headerCanon, bodyCanon dkim.Canonicalization, reload bool) { t.Helper() dir, err := ioutil.TempDir("", "maddy-tests-dkim-") @@ -140,24 +157,61 @@ func TestGenerateSignVerify(t *testing.T) { } defer os.RemoveAll(dir) - // Create the key. - m := newTestModifier(t, dir, keyAlgo) + m := newTestModifier(t, dir, keyAlgo, domains) if reload { - m = newTestModifier(t, dir, keyAlgo) + m = newTestModifier(t, dir, keyAlgo, domains) } - testHdr, body := signTestMsg(t, m) - verifyTestMsg(t, filepath.Join(dir, "testkey.dns"), testHdr, body) + testHdr, body := signTestMsg(t, m, envelopeFrom) + verifyTestMsg(t, dir, expectDomain, testHdr, body) + } for _, algo := range [2]string{"rsa2048", "ed25519"} { for _, hdrCanon := range [2]dkim.Canonicalization{dkim.CanonicalizationSimple, dkim.CanonicalizationRelaxed} { for _, bodyCanon := range [2]dkim.Canonicalization{dkim.CanonicalizationSimple, dkim.CanonicalizationRelaxed} { - test(algo, hdrCanon, bodyCanon, false) - test(algo, hdrCanon, bodyCanon, true) + test([]string{"maddy.test"}, "test@maddy.test", []string{"maddy.test"}, algo, hdrCanon, bodyCanon, false) + test([]string{"maddy.test"}, "test@maddy.test", []string{"maddy.test"}, algo, hdrCanon, bodyCanon, true) } } } + + // Key selection tests + test( + []string{"maddy.test"}, // Generated keys. + "test@maddy.test", // Envelope sender. + []string{"maddy.test"}, // Expected signature domains. + "ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false) + test( + []string{"maddy.test"}, + "test@unrelated.maddy.test", + []string{}, + "ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false) + test( + []string{"maddy.test", "related.maddy.test"}, + "test@related.maddy.test", + []string{"related.maddy.test"}, + "ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false) + test( + []string{"fallback.maddy.test", "maddy.test"}, + "postmaster", + []string{"fallback.maddy.test"}, + "ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false) + test( + []string{"fallback.maddy.test", "maddy.test"}, + "", + []string{"fallback.maddy.test"}, + "ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false) + test( + []string{"another.maddy.test", "another.maddy.test", "maddy.test"}, + "test@another.maddy.test", + []string{"another.maddy.test"}, + "ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false) + test( + []string{"another.maddy.test", "another.maddy.test", "maddy.test"}, + "", + []string{"another.maddy.test"}, + "ed25519", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false) } func TestFieldsToSign(t *testing.T) { @@ -181,175 +235,3 @@ func TestFieldsToSign(t *testing.T) { t.Errorf("incorrect set of fields to sign\nwant: %v\ngot: %v", expected, fields) } } - -func TestShouldSign(t *testing.T) { - type testCase struct { - MatchMethods []string - - KeyDomain string - AuthIdentity string - MAILFROM string - From string - EAI bool - - ShouldSign bool - ExpectedId string - } - - test := func(i int, c testCase) { - h := textproto.Header{} - h.Add("From", c.From) - - m := Modifier{ - domain: c.KeyDomain, - senderMatch: make(map[string]struct{}, len(c.MatchMethods)), - log: testutils.Logger(t, "sign_dkim"), - } - for _, method := range c.MatchMethods { - m.senderMatch[method] = struct{}{} - } - - id, ok := m.shouldSign(c.EAI, strconv.Itoa(i), &h, c.MAILFROM, c.AuthIdentity) - if ok != c.ShouldSign { - t.Errorf("%d %+v: expected ShouldSign=%v, got %v", i, c, c.ShouldSign, ok) - } - if id != c.ExpectedId { - t.Errorf("%d %+v: expected id=%v, got %v", i, c, c.ExpectedId, id) - } - } - - cases := []testCase{ - { - MatchMethods: []string{"off"}, - - KeyDomain: "example.org", - From: "foo@example.org", - - ShouldSign: true, - ExpectedId: "@example.org", - }, - { - MatchMethods: []string{"off"}, - - KeyDomain: "ñaca.com", - From: "foo@ñaca.com", - - ShouldSign: true, - ExpectedId: "@xn--aca-6ma.com", - }, - { - MatchMethods: []string{"off"}, - - KeyDomain: "ñaca.com", - From: "foo@ñaca.com", - EAI: true, - - ShouldSign: true, - ExpectedId: "@ñaca.com", - }, - { - MatchMethods: []string{"envelope"}, - - KeyDomain: "example.org", - From: "foo@example.org", - MAILFROM: "baz@example.org", - - ShouldSign: false, - }, - { - MatchMethods: []string{"envelope"}, - - KeyDomain: "example.com", - From: "foo@example.org", - MAILFROM: "foo@example.org", - - ShouldSign: false, - }, - { - MatchMethods: []string{"envelope"}, - - KeyDomain: "example.org", - From: "foo@example.org", - MAILFROM: "foo@example.org", - - ShouldSign: true, - ExpectedId: "foo@example.org", - }, - { - MatchMethods: []string{"envelope"}, - - KeyDomain: "ñaca.com", - From: "foo@ñaca.com", - MAILFROM: "foo@ñaca.com", - - ShouldSign: true, - ExpectedId: "foo@xn--aca-6ma.com", - }, - { - MatchMethods: []string{"envelope"}, - - KeyDomain: "example.com", - From: "ñ@example.com", - MAILFROM: "ñ@example.com", - - ShouldSign: true, - ExpectedId: "@example.com", - }, - { - MatchMethods: []string{"envelope"}, - - KeyDomain: "example.com", - From: "ñ@example.com", - MAILFROM: "ñ@example.com", - EAI: true, - - ShouldSign: true, - ExpectedId: "ñ@example.com", - }, - { - MatchMethods: []string{"envelope"}, - - KeyDomain: "ñaca.com", - From: "foo@ñaca.com", - MAILFROM: "foo@ñaca.com", - EAI: true, - - ShouldSign: true, - ExpectedId: "foo@ñaca.com", - }, - { - MatchMethods: []string{"envelope", "auth"}, - - KeyDomain: "example.org", - From: "foo@example.org", - MAILFROM: "foo@example.org", - AuthIdentity: "baz", - - ShouldSign: false, - }, - { - MatchMethods: []string{"auth"}, - - KeyDomain: "example.org", - From: "foo@example.org", - MAILFROM: "foo@example.com", - AuthIdentity: "foo", - - ShouldSign: true, - ExpectedId: "foo@example.org", - }, - { - MatchMethods: []string{"envelope", "auth"}, - - KeyDomain: "example.com", - From: "foo@example.com", - MAILFROM: "foo@example.com", - AuthIdentity: "foo@example.org", - - ShouldSign: false, - }, - } - for i, case_ := range cases { - test(i, case_) - } -} diff --git a/maddy.conf b/maddy.conf index 96b1efe..691a131 100644 --- a/maddy.conf +++ b/maddy.conf @@ -95,7 +95,7 @@ limits outbound_limits { checks outbound_checks { } modifiers outbound_modifiers { - sign_dkim $(primary_domain) default + sign_dkim $(primary_domain) $(local_domains) default } mx_auth outbound_auth {