mirror of
https://blitiri.com.ar/repos/chasquid
synced 2026-10-28 15:02:28 +00:00
71af456be7
Today, when looking up DKIM keys via DNS, we go through all the TXT records that are published. This is already more lenient than what's allowed by many implementations (as documented in the code). Potentially, someone could craft a domain with a lot of TXT records to make us waste time processing those keys. To help reduce that possibility, this patch limits the lookup to 3 records. This is still more than normal and may be limited further to 1 in the future.
273 lines
6.8 KiB
Go
273 lines
6.8 KiB
Go
package dkim
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"encoding/base64"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
)
|
|
|
|
func TestLookupError(t *testing.T) {
|
|
testErr := errors.New("lookup error")
|
|
errLookupF := func(ctx context.Context, name string) ([]string, error) {
|
|
return nil, testErr
|
|
}
|
|
ctx := WithLookupTXTFunc(t.Context(), errLookupF)
|
|
|
|
pks, err := findPublicKeys(ctx, "example.com", "selector")
|
|
if pks != nil || err != testErr {
|
|
t.Errorf("findPublicKeys expected nil / lookup error, got %v / %v",
|
|
pks, err)
|
|
}
|
|
}
|
|
|
|
// RSA key from the RFC example.
|
|
// https://datatracker.ietf.org/doc/html/rfc6376#appendix-C
|
|
const exampleRSAKeyB64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ" +
|
|
"KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt" +
|
|
"IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v" +
|
|
"/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi" +
|
|
"tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB"
|
|
|
|
var exampleRSAKeyBuf, _ = base64.StdEncoding.DecodeString(exampleRSAKeyB64)
|
|
|
|
// Ed25519 key from the RFC example.
|
|
// https://datatracker.ietf.org/doc/html/rfc8463#appendix-A.2
|
|
const exampleEd25519KeyB64 = "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="
|
|
|
|
var exampleEd25519KeyBuf, _ = base64.StdEncoding.DecodeString(
|
|
exampleEd25519KeyB64)
|
|
|
|
var results = map[string][]string{}
|
|
var resultErr = map[string]error{}
|
|
|
|
func testLookupTXT(ctx context.Context, name string) ([]string, error) {
|
|
return results[name], resultErr[name]
|
|
}
|
|
|
|
func TestSkipBadRecords(t *testing.T) {
|
|
ctx := WithLookupTXTFunc(t.Context(), testLookupTXT)
|
|
results["selector._domainkey.example.com"] = []string{
|
|
"not a tag",
|
|
"v=DKIM1; p=" + exampleRSAKeyB64,
|
|
}
|
|
defer clear(results)
|
|
|
|
pks, err := findPublicKeys(ctx, "example.com", "selector")
|
|
if err != nil {
|
|
t.Errorf("findPublicKeys expected nil, got %v", err)
|
|
}
|
|
if len(pks) != 1 {
|
|
t.Errorf("findPublicKeys expected 1 key, got %v", len(pks))
|
|
}
|
|
}
|
|
|
|
func TestMaxKeyRecords(t *testing.T) {
|
|
ctx := WithLookupTXTFunc(t.Context(), testLookupTXT)
|
|
defer clear(results)
|
|
|
|
// Only the first maxKeyRecords records are used, even if the rest are
|
|
// valid keys.
|
|
key := "v=DKIM1; p=" + exampleRSAKeyB64
|
|
results["selector._domainkey.example.com"] = []string{
|
|
key, key, key, key, key,
|
|
}
|
|
pks, err := findPublicKeys(ctx, "example.com", "selector")
|
|
if err != nil || len(pks) != maxKeyRecords {
|
|
t.Errorf("findPublicKeys expected %d keys / nil, got %d / %v",
|
|
maxKeyRecords, len(pks), err)
|
|
}
|
|
|
|
// Records after the first maxKeyRecords are ignored, even if the first
|
|
// ones are not valid keys.
|
|
results["selector._domainkey.example.com"] = []string{
|
|
"not a tag", "not a tag", "not a tag", key,
|
|
}
|
|
pks, err = findPublicKeys(ctx, "example.com", "selector")
|
|
if err != nil || len(pks) != 0 {
|
|
t.Errorf("findPublicKeys expected 0 keys / nil, got %d / %v",
|
|
len(pks), err)
|
|
}
|
|
}
|
|
|
|
func TestParsePublicKey(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
pk *publicKey
|
|
err error
|
|
}{
|
|
// Invalid records.
|
|
{"not a tag", nil, errInvalidTag},
|
|
{"v=DKIM666;", nil, errInvalidVersion},
|
|
{"p=abc~*#def", nil, base64.CorruptInputError(3)},
|
|
{"k=blah; p=" + exampleRSAKeyB64, nil, errUnsupportedKeyType},
|
|
|
|
// Error parsing the keys.
|
|
{"p=", nil, errInvalidRSAPublicKey},
|
|
|
|
// RSA key but the contents are a (valid) ECDSA key.
|
|
{"p=" +
|
|
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIT0qsh+0jdY" +
|
|
"DhK5+rSedhT7W/5rTRiulhphqtuplGFAyNiSh9I5t6MsrIu" +
|
|
"xFQV7A/cWAt8qcbVscT3Q2l6iu3w==",
|
|
nil, errNotRSAPublicKey},
|
|
|
|
// Valid RSA key, that is too short.
|
|
{"p=" +
|
|
"MEgCQQCo9+BpMRYQ/dL3DS2CyJxRF+j6ctbT3/Qp84+KeFh" +
|
|
"nii7NT7fELilKUSnxS30WAvQCCo2yU1orfgqr41mM70MBAg" +
|
|
"MBAAE=", nil, errRSAKeyTooSmall},
|
|
|
|
// Invalid ed25519 key.
|
|
{"k=ed25519; p=MFkwEwYH", nil, errInvalidEd25519Key},
|
|
|
|
// Valid.
|
|
{"p=" + exampleRSAKeyB64,
|
|
&publicKey{K: keyTypeRSA, P: exampleRSAKeyBuf}, nil},
|
|
{"k=rsa ; p=" + exampleRSAKeyB64,
|
|
&publicKey{K: keyTypeRSA, P: exampleRSAKeyBuf}, nil},
|
|
{
|
|
"k=rsa; h=sha256; p=" + exampleRSAKeyB64,
|
|
&publicKey{
|
|
K: keyTypeRSA,
|
|
H: []crypto.Hash{crypto.SHA256},
|
|
P: exampleRSAKeyBuf},
|
|
nil,
|
|
},
|
|
{"t=s; p=" + exampleRSAKeyB64,
|
|
&publicKey{
|
|
K: keyTypeRSA,
|
|
P: exampleRSAKeyBuf,
|
|
T: []string{"s"},
|
|
},
|
|
nil,
|
|
},
|
|
{"t = s : y; p=" + exampleRSAKeyB64,
|
|
&publicKey{
|
|
K: keyTypeRSA,
|
|
P: exampleRSAKeyBuf,
|
|
T: []string{"s", "y"},
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
// We should ignore unrecognized hash algorithms.
|
|
"k=rsa; h=sha1:xxx123:sha256; p=" + exampleRSAKeyB64,
|
|
&publicKey{
|
|
K: keyTypeRSA,
|
|
H: []crypto.Hash{crypto.SHA256},
|
|
P: exampleRSAKeyBuf},
|
|
nil,
|
|
},
|
|
{"k=ed25519; p=" + exampleEd25519KeyB64,
|
|
&publicKey{K: keyTypeEd25519, P: exampleEd25519KeyBuf}, nil},
|
|
}
|
|
|
|
for i, c := range cases {
|
|
pk, err := parsePublicKey(c.in)
|
|
diff := cmp.Diff(c.pk, pk,
|
|
cmpopts.IgnoreUnexported(publicKey{}),
|
|
cmpopts.EquateEmpty(),
|
|
)
|
|
if diff != "" {
|
|
t.Errorf("%d: parsePublicKey(%q) key: (-want +got)\n%s",
|
|
i, c.in, diff)
|
|
}
|
|
if !errors.Is(err, c.err) {
|
|
t.Errorf("%d: parsePublicKey(%q) error: want %v, got %v",
|
|
i, c.in, c.err, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPublicKeyMatches(t *testing.T) {
|
|
cases := []struct {
|
|
pk *publicKey
|
|
kt keyType
|
|
h crypto.Hash
|
|
ok bool
|
|
}{
|
|
{
|
|
&publicKey{K: keyTypeRSA},
|
|
keyTypeRSA, crypto.SHA256,
|
|
true,
|
|
},
|
|
{
|
|
&publicKey{K: keyTypeRSA, H: []crypto.Hash{crypto.SHA1}},
|
|
keyTypeRSA, crypto.SHA1,
|
|
true,
|
|
},
|
|
{
|
|
&publicKey{K: keyTypeRSA, H: []crypto.Hash{crypto.SHA1}},
|
|
keyTypeRSA, crypto.SHA256,
|
|
false,
|
|
},
|
|
{
|
|
&publicKey{K: keyTypeRSA, H: []crypto.Hash{crypto.SHA1}},
|
|
keyTypeEd25519, crypto.SHA1,
|
|
false,
|
|
},
|
|
}
|
|
|
|
for i, c := range cases {
|
|
if ok := c.pk.Matches(c.kt, c.h); ok != c.ok {
|
|
t.Errorf("%d: matches(%v, %v) = %v, want %v",
|
|
i, c.kt, c.h, ok, c.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStrictDomainCheck(t *testing.T) {
|
|
cases := []struct {
|
|
t string
|
|
ok bool
|
|
}{
|
|
{"", false},
|
|
{"y", false},
|
|
{"x:y", false},
|
|
{":x::y", false},
|
|
{"s", true},
|
|
{"y:s", true},
|
|
{" y: s", true},
|
|
{"y:s:x", true},
|
|
}
|
|
|
|
for i, c := range cases {
|
|
pkS := "k=ed25519; p=" + exampleEd25519KeyB64 + "; t=" + c.t
|
|
pk, err := parsePublicKey(pkS)
|
|
if err != nil {
|
|
t.Fatalf("%d: parsePublicKey(%q) = %v", i, pkS, err)
|
|
}
|
|
if ok := pk.StrictDomainCheck(); ok != c.ok {
|
|
t.Errorf("%d: strictDomainCheck(t=%q) = %v, want %v",
|
|
i, c.t, ok, c.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func FuzzParsePublicKey(f *testing.F) {
|
|
// Add some initial corpus from the tests above.
|
|
f.Add("not a tag")
|
|
f.Add("v=DKIM666;")
|
|
f.Add("p=abc~*#def")
|
|
f.Add("k=blah; p=" + exampleRSAKeyB64)
|
|
f.Add("p=")
|
|
f.Add("k=ed25519; p=")
|
|
f.Add("k=ed25519; p=MFkwEwYH")
|
|
f.Add("p=" + exampleEd25519KeyB64)
|
|
f.Add("k=rsa ; p=" + exampleRSAKeyB64)
|
|
f.Add("v=DKIM1; p=" + exampleRSAKeyB64)
|
|
f.Add("t=s; p=" + exampleRSAKeyB64)
|
|
f.Add("t = s : y; p=" + exampleRSAKeyB64)
|
|
f.Add("k=rsa; h=sha256; p=" + exampleRSAKeyB64)
|
|
f.Add("k=rsa; h=sha1:xxx123:sha256; p=" + exampleRSAKeyB64)
|
|
|
|
f.Fuzz(func(t *testing.T, in string) {
|
|
parsePublicKey(in)
|
|
})
|
|
}
|