1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2026-10-28 15:02:28 +00:00
Files
go-chasquid-smtp/internal/dkim/verify_test.go
T
Alberto Bertogli 8b19760f4e test: Use modern testing features instead of custom helpers
Many of our tests rely on helpers that predate features which are now
available in the standard library.

This patch replaces them with the standard equivalents:

- MustTempDir/RemoveIfOk -> t.ArtifactDir() or t.TempDir() (depending on
  whether it is useful to keep the data on request).
- GetFreePort + waitForServer -> opening the listeners before starting
  the servers. To support this, localrpc.Server gains a Serve method. We
  may re-introduce these in the future if more use cases appear.
- WaitFor and real-time timing checks -> testing/synctest.
- context.Background() -> t.Context().
- Benchmark b.N loops -> b.Loop().
- reflect.DeepEqual -> cmp.Diff, and proto.Equal for protobufs.
2026-10-03 10:34:35 +01:00

416 lines
12 KiB
Go

package dkim
import (
"context"
"net"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
func toCRLF(s string) string {
return strings.ReplaceAll(s, "\n", "\r\n")
}
func makeLookupTXT(results map[string][]string) lookupTXTFunc {
return func(ctx context.Context, domain string) ([]string, error) {
return results[domain], nil
}
}
func TestVerifyRF6376CExample(t *testing.T) {
ctx := t.Context()
ctx = WithTraceFunc(ctx, t.Logf)
// Use the public key from the example in RFC 6376 appendix C.
// https://datatracker.ietf.org/doc/html/rfc6376#appendix-C
ctx = WithLookupTXTFunc(ctx, makeLookupTXT(map[string][]string{
"brisbane._domainkey.example.com": {
"v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ" +
"KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt" +
"IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v" +
"/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi" +
"tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB",
},
}))
// Note that the examples in the RFC text have multiple issues:
// - The double space in "game. Are" should be a single
// space. Otherwise, the body hash does not match.
// https://www.rfc-editor.org/errata/eid3192
// - The header indentation is incorrect. This causes
// signature validation failure (because the example uses simple
// canonicalization, which leaves the indentation untouched).
// https://www.rfc-editor.org/errata/eid4926
message := toCRLF(
`DKIM-Signature: v=1; a=rsa-sha256; s=brisbane; d=example.com;
c=simple/simple; q=dns/txt; i=joe@football.example.com;
h=Received : From : To : Subject : Date : Message-ID;
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB
4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut
KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV
4bmp/YzhwvcubU4=;
Received: from client1.football.example.com [192.0.2.1]
by submitserver.example.com with SUBMISSION;
Fri, 11 Jul 2003 21:01:54 -0700 (PDT)
From: Joe SixPack <joe@football.example.com>
To: Suzie Q <suzie@shopping.example.net>
Subject: Is dinner ready?
Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
Message-ID: <20030712040037.46341.5F8J@football.example.com>
Hi.
We lost the game. Are you hungry yet?
Joe.
`)
res, err := VerifyMessage(ctx, message)
if res.Valid != 1 || err != nil {
t.Errorf("VerifyMessage: wanted 1 valid / nil; got %v / %v", res, err)
}
// Extend the message, check it does not pass validation.
res, err = VerifyMessage(ctx, message+"Extra line.\r\n")
if res.Valid != 0 || err != nil {
t.Errorf("VerifyMessage: wanted 0 valid / nil; got %v / %v", res, err)
}
// Alter a header, check it does not pass validation.
res, err = VerifyMessage(ctx,
strings.Replace(message, "Subject", "X-Subject", 1))
if res.Valid != 0 || err != nil {
t.Errorf("VerifyMessage: wanted 0 valid / nil; got %v / %v", res, err)
}
}
func TestVerifyRFC8463Example(t *testing.T) {
ctx := t.Context()
ctx = WithTraceFunc(ctx, t.Logf)
// Use the public keys from the example in RFC 8463 appendix A.2.
// https://datatracker.ietf.org/doc/html/rfc6376#appendix-C
ctx = WithLookupTXTFunc(ctx, makeLookupTXT(map[string][]string{
"brisbane._domainkey.football.example.com": {
"v=DKIM1; k=ed25519; " +
"p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="},
"test._domainkey.football.example.com": {
"v=DKIM1; k=rsa; " +
"p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWR" +
"iGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/b" +
"yYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKr" +
"M3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K" +
"4w3QIDAQAB"},
}))
message := toCRLF(
`DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed;
d=football.example.com; i=@football.example.com;
q=dns/txt; s=brisbane; t=1528637909; h=from : to :
subject : date : message-id : from : subject : date;
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
b=/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQUdt9OdqQehSwhEIug4D11Bus
Fa3bT3FY5OsU7ZbnKELq+eXdp1Q1Dw==
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=football.example.com; i=@football.example.com;
q=dns/txt; s=test; t=1528637909; h=from : to : subject :
date : message-id : from : subject : date;
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
b=F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8GwpsRe0IeIixNTe3
DhCVlUrSjV4BwcVcOF6+FF3Zo9Rpo1tFOeS9mPYQTnGdaSGsgeefOsk2Jz
dA+L10TeYt9BgDfQNZtKdN1WO//KgIqXP7OdEFE4LjFYNcUxZQ4FADY+8=
From: Joe SixPack <joe@football.example.com>
To: Suzie Q <suzie@shopping.example.net>
Subject: Is dinner ready?
Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
Message-ID: <20030712040037.46341.5F8J@football.example.com>
Hi.
We lost the game. Are you hungry yet?
Joe.
`)
expected := &VerifyResult{
Found: 2,
Valid: 2,
Results: []*OneResult{
{
SignatureHeader: toCRLF(
` v=1; a=ed25519-sha256; c=relaxed/relaxed;
d=football.example.com; i=@football.example.com;
q=dns/txt; s=brisbane; t=1528637909; h=from : to :
subject : date : message-id : from : subject : date;
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
b=/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQUdt9OdqQehSwhEIug4D11Bus
Fa3bT3FY5OsU7ZbnKELq+eXdp1Q1Dw==`),
Domain: "football.example.com",
Selector: "brisbane",
B: "/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQUdt9OdqQehSwhEIug4D11" +
"BusFa3bT3FY5OsU7ZbnKELq+eXdp1Q1Dw==",
State: SUCCESS,
Error: nil,
},
{
SignatureHeader: toCRLF(
` v=1; a=rsa-sha256; c=relaxed/relaxed;
d=football.example.com; i=@football.example.com;
q=dns/txt; s=test; t=1528637909; h=from : to : subject :
date : message-id : from : subject : date;
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
b=F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8GwpsRe0IeIixNTe3
DhCVlUrSjV4BwcVcOF6+FF3Zo9Rpo1tFOeS9mPYQTnGdaSGsgeefOsk2Jz
dA+L10TeYt9BgDfQNZtKdN1WO//KgIqXP7OdEFE4LjFYNcUxZQ4FADY+8=`),
Domain: "football.example.com",
Selector: "test",
B: "F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8GwpsRe0IeIixNTe" +
"3DhCVlUrSjV4BwcVcOF6+FF3Zo9Rpo1tFOeS9mPYQTnGdaSGsgeefO" +
"sk2JzdA+L10TeYt9BgDfQNZtKdN1WO//KgIqXP7OdEFE4LjFYNcUxZ" +
"Q4FADY+8=",
State: SUCCESS,
Error: nil,
},
},
}
res, err := VerifyMessage(ctx, message)
if err != nil {
t.Fatalf("VerifyMessage returned error: %v", err)
}
if diff := cmp.Diff(expected, res); diff != "" {
t.Errorf("VerifyMessage diff (-want +got):\n%s", diff)
}
// Extend the message, check it does not pass validation.
res, err = VerifyMessage(ctx, message+"Extra line.\r\n")
if res.Found != 2 || res.Valid != 0 || err != nil {
t.Errorf("VerifyMessage: wanted 2 found, 0 valid / nil; got %v / %v",
res, err)
}
// Alter a header, check it does not pass validation.
res, err = VerifyMessage(ctx,
strings.Replace(message, "Subject", "X-Subject", 1))
if res.Found != 2 || res.Valid != 0 || err != nil {
t.Errorf("VerifyMessage: wanted 2 found, 0 valid / nil; got %v / %v",
res, err)
}
}
func TestHeadersToInclude(t *testing.T) {
// Test that headersToInclude returns the expected headers.
cases := []struct {
sigH header
hTag []string
headers headers
want []header
}{
// Check that if a header appears more than once, we pick the latest
// first.
{
sigH: header{
Name: "DKIM-Signature",
Value: "v=1; a=rsa-sha256; s=brisbane; d=example.com;",
},
hTag: []string{"From", "To", "Subject"},
headers: headers{
{Name: "From", Value: "from1"},
{Name: "To", Value: "to1"},
{Name: "Subject", Value: "subject1"},
{Name: "From", Value: "from2"},
},
want: []header{
{Name: "From", Value: "from2"},
{Name: "To", Value: "to1"},
{Name: "Subject", Value: "subject1"},
},
},
// Check that if a header is requested twice but only appears once, we
// only return it once.
// This is a common technique suggested by the RFC to make signatures
// fail if a header is added.
{
sigH: header{
Name: "DKIM-Signature",
Value: "v=1; a=rsa-sha256; s=brisbane; d=example.com;",
},
hTag: []string{"From", "From", "To", "Subject"},
headers: headers{
{Name: "From", Value: "from1"},
{Name: "To", Value: "to1"},
{Name: "Subject", Value: "subject1"},
},
want: []header{
{Name: "From", Value: "from1"},
{Name: "To", Value: "to1"},
{Name: "Subject", Value: "subject1"},
},
},
// Check that if DKIM-Signature is included, we do *not* include the
// one we're currently verifying in the headers to include.
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.7
{
sigH: header{
Name: "DKIM-Signature",
Value: "v=1; a=rsa-sha256; s=brisbane; d=example.com;",
},
hTag: []string{"From", "From", "DKIM-Signature", "DKIM-Signature"},
headers: headers{
{Name: "From", Value: "from1"},
{Name: "To", Value: "to1"},
{
Name: "DKIM-Signature",
Value: "v=1; a=rsa-sha256; s=sidney; d=example.com;",
},
{
Name: "DKIM-Signature",
Value: "v=1; a=rsa-sha256; s=brisbane; d=example.com;",
},
},
want: []header{
{Name: "From", Value: "from1"},
{
Name: "DKIM-Signature",
Value: "v=1; a=rsa-sha256; s=sidney; d=example.com;",
},
},
},
}
for _, c := range cases {
got := headersToInclude(c.sigH, c.hTag, c.headers)
if diff := cmp.Diff(c.want, got); diff != "" {
t.Errorf("headersToInclude(%q, %v, %v) diff (-want +got):\n%s",
c.sigH, c.hTag, c.headers, diff)
}
}
}
func TestAuthenticationResults(t *testing.T) {
resBrisbane := &OneResult{
Domain: "football.example.com",
Selector: "brisbane",
B: "/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQUdt9OdqQehSwhEIug4D11" +
"BusFa3bT3FY5OsU7ZbnKELq+eXdp1Q1Dw==",
State: SUCCESS,
Error: nil,
}
resTest := &OneResult{
Domain: "football.example.com",
Selector: "test",
B: "F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8GwpsRe0IeIixNTe" +
"3DhCVlUrSjV4BwcVcOF6+FF3Zo9Rpo1tFOeS9mPYQTnGdaSGsgeefO" +
"sk2JzdA+L10TeYt9BgDfQNZtKdN1WO//KgIqXP7OdEFE4LjFYNcUxZ" +
"Q4FADY+8=",
State: SUCCESS,
Error: nil,
}
resFail := &OneResult{
Domain: "football.example.com",
Selector: "paris",
B: "slfkdMSDFeslif39seFfjl93sljisdsdlif923l",
State: PERMFAIL,
Error: ErrVerificationFailed,
}
resPermFail := &OneResult{
Domain: "football.example.com",
Selector: "paris",
// No B tag on purpose.
State: PERMFAIL,
Error: errMissingRequiredTag,
}
resTempFail := &OneResult{
Domain: "football.example.com",
Selector: "paris",
B: "shorty", // Less than 12 characters to check we include it well.
State: TEMPFAIL,
Error: &net.DNSError{
Err: "dns temp error (for testing)",
IsTemporary: true,
},
}
cases := []struct {
results *VerifyResult
want string
}{
{
results: &VerifyResult{},
want: ";dkim=none\r\n",
},
{
results: &VerifyResult{
Found: 1,
Valid: 1,
Results: []*OneResult{resBrisbane},
},
want: ";dkim=pass" +
" header.b=/gCrinpcQOoI header.d=football.example.com\r\n",
},
{
results: &VerifyResult{
Found: 2,
Valid: 2,
Results: []*OneResult{resBrisbane, resTest},
},
want: ";dkim=pass" +
" header.b=/gCrinpcQOoI header.d=football.example.com\r\n" +
";dkim=pass" +
" header.b=F45dVWDfMbQD header.d=football.example.com\r\n",
},
{
results: &VerifyResult{
Found: 2,
Valid: 2,
Results: []*OneResult{resBrisbane, resTest},
},
want: ";dkim=pass" +
" header.b=/gCrinpcQOoI header.d=football.example.com\r\n" +
";dkim=pass" +
" header.b=F45dVWDfMbQD header.d=football.example.com\r\n",
},
{
results: &VerifyResult{
Found: 2,
Valid: 1,
Results: []*OneResult{resFail, resTest},
},
want: ";dkim=fail reason=\"verification failed\"\r\n" +
" header.b=slfkdMSDFesl header.d=football.example.com\r\n" +
";dkim=pass" +
" header.b=F45dVWDfMbQD header.d=football.example.com\r\n",
},
{
results: &VerifyResult{
Found: 1,
Results: []*OneResult{resPermFail},
},
want: ";dkim=permerror reason=\"missing required tag\"\r\n" +
" header.d=football.example.com\r\n",
},
{
results: &VerifyResult{
Found: 1,
Results: []*OneResult{resTempFail},
},
want: ";dkim=temperror reason=\"lookup : dns temp error (for testing)\"\r\n" +
" header.b=shorty header.d=football.example.com\r\n",
},
}
for i, c := range cases {
got := c.results.AuthenticationResults()
if diff := cmp.Diff(c.want, got); diff != "" {
t.Errorf("case %d: AuthenticationResults() diff (-want +got):\n%s",
i, diff)
}
}
}