mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 18:24:35 +03:00
expose fewer internals in packages, for easier software reuse
- prometheus is now behind an interface, they aren't dependencies for the reusable components anymore. - some dependencies have been inverted: instead of packages importing a main package to get configuration, the main package now sets configuration in these packages. that means fewer internals are pulled in. - some functions now have new parameters for values that were retrieved from package "mox-".
This commit is contained in:
86
dkim/dkim.go
86
dkim/dkim.go
@ -26,38 +26,17 @@ import (
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/mjl-/mox/config"
|
||||
"github.com/mjl-/mox/dns"
|
||||
"github.com/mjl-/mox/mlog"
|
||||
"github.com/mjl-/mox/moxio"
|
||||
"github.com/mjl-/mox/publicsuffix"
|
||||
"github.com/mjl-/mox/smtp"
|
||||
"github.com/mjl-/mox/stub"
|
||||
)
|
||||
|
||||
var (
|
||||
metricSign = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "mox_dkim_sign_total",
|
||||
Help: "DKIM messages signings, label key is the type of key, rsa or ed25519.",
|
||||
},
|
||||
[]string{
|
||||
"key",
|
||||
},
|
||||
)
|
||||
metricVerify = promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "mox_dkim_verify_duration_seconds",
|
||||
Help: "DKIM verify, including lookup, duration and result.",
|
||||
Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
|
||||
},
|
||||
[]string{
|
||||
"algorithm",
|
||||
"status",
|
||||
},
|
||||
)
|
||||
MetricSign stub.CounterVec = stub.CounterVecIgnore{}
|
||||
MetricVerify stub.HistogramVec = stub.HistogramVecIgnore{}
|
||||
)
|
||||
|
||||
var timeNow = time.Now // Replaced during tests.
|
||||
@ -122,8 +101,28 @@ type Result struct {
|
||||
|
||||
// todo: use some io.Writer to hash the body and the header.
|
||||
|
||||
// Selector holds selectors and key material to generate DKIM signatures.
|
||||
type Selector struct {
|
||||
Hash string // "sha256" or the older "sha1".
|
||||
HeaderRelaxed bool // If the header is canonicalized in relaxed instead of simple mode.
|
||||
BodyRelaxed bool // If the body is canonicalized in relaxed instead of simple mode.
|
||||
Headers []string // Headers to include in signature.
|
||||
|
||||
// Whether to "oversign" headers, ensuring additional/new values of existing
|
||||
// headers cannot be added.
|
||||
SealHeaders bool
|
||||
|
||||
// If > 0, period a signature is valid after signing, as duration, e.g. 72h. The
|
||||
// period should be enough for delivery at the final destination, potentially with
|
||||
// several hops/relays. In the order of days at least.
|
||||
Expiration time.Duration
|
||||
|
||||
PrivateKey crypto.Signer // Either an *rsa.PrivateKey or ed25519.PrivateKey.
|
||||
Domain dns.Domain // Of selector only, not FQDN.
|
||||
}
|
||||
|
||||
// Sign returns line(s) with DKIM-Signature headers, generated according to the configuration.
|
||||
func Sign(ctx context.Context, elog *slog.Logger, localpart smtp.Localpart, domain dns.Domain, c config.DKIM, smtputf8 bool, msg io.ReaderAt) (headers string, rerr error) {
|
||||
func Sign(ctx context.Context, elog *slog.Logger, localpart smtp.Localpart, domain dns.Domain, selectors []Selector, smtputf8 bool, msg io.ReaderAt) (headers string, rerr error) {
|
||||
log := mlog.New("dkim", elog)
|
||||
start := timeNow()
|
||||
defer func() {
|
||||
@ -155,26 +154,25 @@ func Sign(ctx context.Context, elog *slog.Logger, localpart smtp.Localpart, doma
|
||||
|
||||
var bodyHashes = map[hashKey][]byte{}
|
||||
|
||||
for _, sign := range c.Sign {
|
||||
sel := c.Selectors[sign]
|
||||
for _, sel := range selectors {
|
||||
sig := newSigWithDefaults()
|
||||
sig.Version = 1
|
||||
switch sel.Key.(type) {
|
||||
switch sel.PrivateKey.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
sig.AlgorithmSign = "rsa"
|
||||
metricSign.WithLabelValues("rsa").Inc()
|
||||
MetricSign.IncLabels("rsa")
|
||||
case ed25519.PrivateKey:
|
||||
sig.AlgorithmSign = "ed25519"
|
||||
metricSign.WithLabelValues("ed25519").Inc()
|
||||
MetricSign.IncLabels("ed25519")
|
||||
default:
|
||||
return "", fmt.Errorf("internal error, unknown pivate key %T", sel.Key)
|
||||
return "", fmt.Errorf("internal error, unknown pivate key %T", sel.PrivateKey)
|
||||
}
|
||||
sig.AlgorithmHash = sel.HashEffective
|
||||
sig.AlgorithmHash = sel.Hash
|
||||
sig.Domain = domain
|
||||
sig.Selector = sel.Domain
|
||||
sig.Identity = &Identity{&localpart, domain}
|
||||
sig.SignedHeaders = append([]string{}, sel.HeadersEffective...)
|
||||
if !sel.DontSealHeaders {
|
||||
sig.SignedHeaders = append([]string{}, sel.Headers...)
|
||||
if sel.SealHeaders {
|
||||
// ../rfc/6376:2156
|
||||
// Each time a header name is added to the signature, the next unused value is
|
||||
// signed (in reverse order as they occur in the message). So we can add each
|
||||
@ -184,23 +182,23 @@ func Sign(ctx context.Context, elog *slog.Logger, localpart smtp.Localpart, doma
|
||||
for _, h := range hdrs {
|
||||
counts[h.lkey]++
|
||||
}
|
||||
for _, h := range sel.HeadersEffective {
|
||||
for _, h := range sel.Headers {
|
||||
for j := counts[strings.ToLower(h)]; j > 0; j-- {
|
||||
sig.SignedHeaders = append(sig.SignedHeaders, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
sig.SignTime = timeNow().Unix()
|
||||
if sel.ExpirationSeconds > 0 {
|
||||
sig.ExpireTime = sig.SignTime + int64(sel.ExpirationSeconds)
|
||||
if sel.Expiration > 0 {
|
||||
sig.ExpireTime = sig.SignTime + int64(sel.Expiration/time.Second)
|
||||
}
|
||||
|
||||
sig.Canonicalization = "simple"
|
||||
if sel.Canonicalization.HeaderRelaxed {
|
||||
if sel.HeaderRelaxed {
|
||||
sig.Canonicalization = "relaxed"
|
||||
}
|
||||
sig.Canonicalization += "/"
|
||||
if sel.Canonicalization.BodyRelaxed {
|
||||
if sel.BodyRelaxed {
|
||||
sig.Canonicalization += "relaxed"
|
||||
} else {
|
||||
sig.Canonicalization += "simple"
|
||||
@ -217,12 +215,12 @@ func Sign(ctx context.Context, elog *slog.Logger, localpart smtp.Localpart, doma
|
||||
// DKIM-Signature header.
|
||||
// ../rfc/6376:1700
|
||||
|
||||
hk := hashKey{!sel.Canonicalization.BodyRelaxed, strings.ToLower(sig.AlgorithmHash)}
|
||||
hk := hashKey{!sel.BodyRelaxed, strings.ToLower(sig.AlgorithmHash)}
|
||||
if bh, ok := bodyHashes[hk]; ok {
|
||||
sig.BodyHash = bh
|
||||
} else {
|
||||
br := bufio.NewReader(&moxio.AtReader{R: msg, Offset: int64(bodyOffset)})
|
||||
bh, err = bodyHash(h.New(), !sel.Canonicalization.BodyRelaxed, br)
|
||||
bh, err = bodyHash(h.New(), !sel.BodyRelaxed, br)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -236,12 +234,12 @@ func Sign(ctx context.Context, elog *slog.Logger, localpart smtp.Localpart, doma
|
||||
}
|
||||
verifySig := []byte(strings.TrimSuffix(sigh, "\r\n"))
|
||||
|
||||
dh, err := dataHash(h.New(), !sel.Canonicalization.HeaderRelaxed, sig, hdrs, verifySig)
|
||||
dh, err := dataHash(h.New(), !sel.HeaderRelaxed, sig, hdrs, verifySig)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch key := sel.Key.(type) {
|
||||
switch key := sel.PrivateKey.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
sig.Signature, err = key.Sign(cryptorand.Reader, dh, h)
|
||||
if err != nil {
|
||||
@ -358,7 +356,7 @@ func Verify(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, smtpu
|
||||
alg = r.Sig.Algorithm()
|
||||
}
|
||||
status := string(r.Status)
|
||||
metricVerify.WithLabelValues(alg, status).Observe(duration)
|
||||
MetricVerify.ObserveLabels(duration, alg, status)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
|
@ -15,7 +15,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mjl-/mox/config"
|
||||
"github.com/mjl-/mox/dns"
|
||||
"github.com/mjl-/mox/mlog"
|
||||
)
|
||||
@ -222,50 +221,42 @@ test
|
||||
rsaKey := getRSAKey(t)
|
||||
ed25519Key := ed25519.NewKeyFromSeed(make([]byte, 32))
|
||||
|
||||
selrsa := config.Selector{
|
||||
HashEffective: "sha256",
|
||||
Key: rsaKey,
|
||||
HeadersEffective: strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-ID,Content-Type", ","),
|
||||
Domain: dns.Domain{ASCII: "testrsa"},
|
||||
selrsa := Selector{
|
||||
Hash: "sha256",
|
||||
PrivateKey: rsaKey,
|
||||
Headers: strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-ID,Content-Type", ","),
|
||||
Domain: dns.Domain{ASCII: "testrsa"},
|
||||
}
|
||||
|
||||
// Now with sha1 and relaxed canonicalization.
|
||||
selrsa2 := config.Selector{
|
||||
HashEffective: "sha1",
|
||||
Key: rsaKey,
|
||||
HeadersEffective: strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-ID,Content-Type", ","),
|
||||
Domain: dns.Domain{ASCII: "testrsa2"},
|
||||
selrsa2 := Selector{
|
||||
Hash: "sha1",
|
||||
PrivateKey: rsaKey,
|
||||
Headers: strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-ID,Content-Type", ","),
|
||||
Domain: dns.Domain{ASCII: "testrsa2"},
|
||||
}
|
||||
selrsa2.Canonicalization.HeaderRelaxed = true
|
||||
selrsa2.Canonicalization.BodyRelaxed = true
|
||||
selrsa2.HeaderRelaxed = true
|
||||
selrsa2.BodyRelaxed = true
|
||||
|
||||
// Ed25519 key.
|
||||
seled25519 := config.Selector{
|
||||
HashEffective: "sha256",
|
||||
Key: ed25519Key,
|
||||
HeadersEffective: strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-ID,Content-Type", ","),
|
||||
Domain: dns.Domain{ASCII: "tested25519"},
|
||||
seled25519 := Selector{
|
||||
Hash: "sha256",
|
||||
PrivateKey: ed25519Key,
|
||||
Headers: strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-ID,Content-Type", ","),
|
||||
Domain: dns.Domain{ASCII: "tested25519"},
|
||||
}
|
||||
// Again ed25519, but without sealing headers. Use sha256 again, for reusing the body hash from the previous dkim-signature.
|
||||
seled25519b := config.Selector{
|
||||
HashEffective: "sha256",
|
||||
Key: ed25519Key,
|
||||
HeadersEffective: strings.Split("From,To,Cc,Bcc,Reply-To,Subject,Date", ","),
|
||||
DontSealHeaders: true,
|
||||
Domain: dns.Domain{ASCII: "tested25519b"},
|
||||
}
|
||||
dkimConf := config.DKIM{
|
||||
Selectors: map[string]config.Selector{
|
||||
"testrsa": selrsa,
|
||||
"testrsa2": selrsa2,
|
||||
"tested25519": seled25519,
|
||||
"tested25519b": seled25519b,
|
||||
},
|
||||
Sign: []string{"testrsa", "testrsa2", "tested25519", "tested25519b"},
|
||||
seled25519b := Selector{
|
||||
Hash: "sha256",
|
||||
PrivateKey: ed25519Key,
|
||||
Headers: strings.Split("From,To,Cc,Bcc,Reply-To,Subject,Date", ","),
|
||||
SealHeaders: true,
|
||||
Domain: dns.Domain{ASCII: "tested25519b"},
|
||||
}
|
||||
selectors := []Selector{selrsa, selrsa2, seled25519, seled25519b}
|
||||
|
||||
ctx := context.Background()
|
||||
headers, err := Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, dkimConf, false, strings.NewReader(message))
|
||||
headers, err := Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader(message))
|
||||
if err != nil {
|
||||
t.Fatalf("sign: %v", err)
|
||||
}
|
||||
@ -307,31 +298,31 @@ test
|
||||
//log.Infof("nmsg\n%s", nmsg)
|
||||
|
||||
// Multiple From headers.
|
||||
_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, dkimConf, false, strings.NewReader("From: <mjl@mox.example>\r\nFrom: <mjl@mox.example>\r\n\r\ntest"))
|
||||
_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader("From: <mjl@mox.example>\r\nFrom: <mjl@mox.example>\r\n\r\ntest"))
|
||||
if !errors.Is(err, ErrFrom) {
|
||||
t.Fatalf("sign, got err %v, expected ErrFrom", err)
|
||||
}
|
||||
|
||||
// No From header.
|
||||
_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, dkimConf, false, strings.NewReader("Brom: <mjl@mox.example>\r\n\r\ntest"))
|
||||
_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader("Brom: <mjl@mox.example>\r\n\r\ntest"))
|
||||
if !errors.Is(err, ErrFrom) {
|
||||
t.Fatalf("sign, got err %v, expected ErrFrom", err)
|
||||
}
|
||||
|
||||
// Malformed headers.
|
||||
_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, dkimConf, false, strings.NewReader(":\r\n\r\ntest"))
|
||||
_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader(":\r\n\r\ntest"))
|
||||
if !errors.Is(err, ErrHeaderMalformed) {
|
||||
t.Fatalf("sign, got err %v, expected ErrHeaderMalformed", err)
|
||||
}
|
||||
_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, dkimConf, false, strings.NewReader(" From:<mjl@mox.example>\r\n\r\ntest"))
|
||||
_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader(" From:<mjl@mox.example>\r\n\r\ntest"))
|
||||
if !errors.Is(err, ErrHeaderMalformed) {
|
||||
t.Fatalf("sign, got err %v, expected ErrHeaderMalformed", err)
|
||||
}
|
||||
_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, dkimConf, false, strings.NewReader("Frøm:<mjl@mox.example>\r\n\r\ntest"))
|
||||
_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader("Frøm:<mjl@mox.example>\r\n\r\ntest"))
|
||||
if !errors.Is(err, ErrHeaderMalformed) {
|
||||
t.Fatalf("sign, got err %v, expected ErrHeaderMalformed", err)
|
||||
}
|
||||
_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, dkimConf, false, strings.NewReader("From:<mjl@mox.example>"))
|
||||
_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader("From:<mjl@mox.example>"))
|
||||
if !errors.Is(err, ErrHeaderMalformed) {
|
||||
t.Fatalf("sign, got err %v, expected ErrHeaderMalformed", err)
|
||||
}
|
||||
@ -358,9 +349,9 @@ test
|
||||
var record *Record
|
||||
var recordTxt string
|
||||
var msg string
|
||||
var sel config.Selector
|
||||
var dkimConf config.DKIM
|
||||
var policy func(*Sig) error
|
||||
var sel Selector
|
||||
var selectors []Selector
|
||||
var signed bool
|
||||
var signDomain dns.Domain
|
||||
|
||||
@ -389,18 +380,13 @@ test
|
||||
},
|
||||
}
|
||||
|
||||
sel = config.Selector{
|
||||
HashEffective: "sha256",
|
||||
Key: key,
|
||||
HeadersEffective: strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-ID,Content-Type", ","),
|
||||
Domain: dns.Domain{ASCII: "test"},
|
||||
}
|
||||
dkimConf = config.DKIM{
|
||||
Selectors: map[string]config.Selector{
|
||||
"test": sel,
|
||||
},
|
||||
Sign: []string{"test"},
|
||||
sel = Selector{
|
||||
Hash: "sha256",
|
||||
PrivateKey: key,
|
||||
Headers: strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-ID,Content-Type", ","),
|
||||
Domain: dns.Domain{ASCII: "test"},
|
||||
}
|
||||
selectors = []Selector{sel}
|
||||
|
||||
msg = message
|
||||
signed = false
|
||||
@ -411,7 +397,7 @@ test
|
||||
|
||||
msg = strings.ReplaceAll(msg, "\n", "\r\n")
|
||||
|
||||
headers, err := Sign(context.Background(), pkglog.Logger, "mjl", signDomain, dkimConf, false, strings.NewReader(msg))
|
||||
headers, err := Sign(context.Background(), pkglog.Logger, "mjl", signDomain, selectors, false, strings.NewReader(msg))
|
||||
if err != nil {
|
||||
t.Fatalf("sign: %v", err)
|
||||
}
|
||||
@ -515,11 +501,9 @@ test
|
||||
})
|
||||
// Unknown canonicalization.
|
||||
test(nil, StatusPermerror, ErrCanonicalizationUnknown, func() {
|
||||
sel.Canonicalization.HeaderRelaxed = true
|
||||
sel.Canonicalization.BodyRelaxed = true
|
||||
dkimConf.Selectors = map[string]config.Selector{
|
||||
"test": sel,
|
||||
}
|
||||
sel.HeaderRelaxed = true
|
||||
sel.BodyRelaxed = true
|
||||
selectors = []Selector{sel}
|
||||
|
||||
sign()
|
||||
msg = strings.ReplaceAll(msg, "relaxed/relaxed", "bogus/bogus")
|
||||
@ -577,10 +561,8 @@ test
|
||||
resolver.TXT = map[string][]string{
|
||||
"test._domainkey.mox.example.": {txt},
|
||||
}
|
||||
sel.Key = key
|
||||
dkimConf.Selectors = map[string]config.Selector{
|
||||
"test": sel,
|
||||
}
|
||||
sel.PrivateKey = key
|
||||
selectors = []Selector{sel}
|
||||
})
|
||||
// Key not allowed for email by DNS record. ../rfc/6376:1541
|
||||
test(nil, StatusPermerror, ErrKeyNotForEmail, func() {
|
||||
@ -603,18 +585,14 @@ test
|
||||
|
||||
// Check that last-occurring header field is used.
|
||||
test(nil, StatusFail, ErrSigVerify, func() {
|
||||
sel.DontSealHeaders = true
|
||||
dkimConf.Selectors = map[string]config.Selector{
|
||||
"test": sel,
|
||||
}
|
||||
sel.SealHeaders = false
|
||||
selectors = []Selector{sel}
|
||||
sign()
|
||||
msg = strings.ReplaceAll(msg, "\r\n\r\n", "\r\nsubject: another\r\n\r\n")
|
||||
})
|
||||
test(nil, StatusPass, nil, func() {
|
||||
sel.DontSealHeaders = true
|
||||
dkimConf.Selectors = map[string]config.Selector{
|
||||
"test": sel,
|
||||
}
|
||||
sel.SealHeaders = false
|
||||
selectors = []Selector{sel}
|
||||
sign()
|
||||
msg = "subject: another\r\n" + msg
|
||||
})
|
||||
|
@ -7,10 +7,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/mjl-/mox/dns"
|
||||
"github.com/mjl-/mox/moxvar"
|
||||
"github.com/mjl-/mox/smtp"
|
||||
)
|
||||
|
||||
// Pedantic enables stricter parsing.
|
||||
var Pedantic bool
|
||||
|
||||
type parseErr string
|
||||
|
||||
func (e parseErr) Error() string {
|
||||
@ -200,7 +202,7 @@ func (p *parser) xdomainselector(isselector bool) dns.Domain {
|
||||
// domain names must always be a-labels, ../rfc/6376:1115 ../rfc/6376:1187 ../rfc/6376:1303
|
||||
// dkim selectors with underscores happen in the wild, accept them when not in
|
||||
// pedantic mode. ../rfc/6376:581 ../rfc/5321:2303
|
||||
return isalphadigit(c) || (i > 0 && (c == '-' || isselector && !moxvar.Pedantic && c == '_') && p.o+1 < len(p.s))
|
||||
return isalphadigit(c) || (i > 0 && (c == '-' || isselector && !Pedantic && c == '_') && p.o+1 < len(p.s))
|
||||
}
|
||||
s := p.xtakefn1(false, subdomain)
|
||||
for p.hasPrefix(".") {
|
||||
@ -273,7 +275,7 @@ func (p *parser) xlocalpart() smtp.Localpart {
|
||||
}
|
||||
}
|
||||
// In the wild, some services use large localparts for generated (bounce) addresses.
|
||||
if moxvar.Pedantic && len(s) > 64 || len(s) > 128 {
|
||||
if Pedantic && len(s) > 64 || len(s) > 128 {
|
||||
// ../rfc/5321:3486
|
||||
p.xerrorf("localpart longer than 64 octets")
|
||||
}
|
||||
|
Reference in New Issue
Block a user