| author | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-10-09 10:43:33 UTC |
| committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-10-09 20:34:17 UTC |
| parent | 49030cb9c83e3f1f310daedd0430397c7b37066d |
| internal/dkim/sign.go | +5 | -1 |
| internal/dkim/sign_test.go | +45 | -0 |
diff --git a/internal/dkim/sign.go b/internal/dkim/sign.go index 1d89394..03bcd7a 100644 --- a/internal/dkim/sign.go +++ b/internal/dkim/sign.go @@ -9,6 +9,7 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + "slices" "strings" "time" ) @@ -96,7 +97,10 @@ func (s *Signer) Sign(ctx context.Context, message string) (string, error) { // First, the canonicalized headers. b := sha256.New() for _, h := range headersToSign { - for _, header := range headers.FindAll(h) { + // If a header appears multiple times, we must hash the instances + // from the bottom of the header block to the top. + // https://datatracker.ietf.org/doc/html/rfc6376#section-5.4.2 + for _, header := range slices.Backward(headers.FindAll(h)) { hsrc := relaxedCanonicalization.header(header).Source + "\r\n" trace(ctx, "Hashing header: %q", hsrc) b.Write([]byte(hsrc)) diff --git a/internal/dkim/sign_test.go b/internal/dkim/sign_test.go index 7184a24..f31e43b 100644 --- a/internal/dkim/sign_test.go +++ b/internal/dkim/sign_test.go @@ -165,6 +165,51 @@ func TestSignManyHeaders(t *testing.T) { } } +func TestSignRepeatedHeaders(t *testing.T) { + // When a signed header appears multiple times (with different values), + // they must be signed from bottom to top, otherwise the signature will + // not verify. + ctx := t.Context() + ctx = WithTraceFunc(ctx, t.Logf) + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("ed25519.GenerateKey: %v", err) + } + + ctx = WithLookupTXTFunc(ctx, makeLookupTXT(map[string][]string{ + "test._domainkey.example.com": { + "v=DKIM1; k=ed25519; p=" + base64.StdEncoding.EncodeToString(pub), + }, + })) + + s := &Signer{ + Domain: "example.com", + Selector: "test", + Signer: priv, + } + + message := toCRLF(`From: from@example.com +To: to1@example.com +Subject: Subject 1 +To: to2@example.com +Subject: Subject 2 +To: to3@example.com + +Hi. +`) + + sig, err := s.Sign(ctx, message) + if err != nil { + t.Fatalf("Sign: %v", err) + } + + res, err := VerifyMessage(ctx, addSig(sig, message)) + if err != nil || res.Valid != 1 { + t.Errorf("VerifyMessage: wanted 1 valid / nil; got %v / %v", res, err) + } +} + func addSig(sig, message string) string { return "DKIM-Signature: " + strings.ReplaceAll(sig, "\r\n", "\r\n\t") +