dkim: expose algorithm, hash list, and key size in DKIM record analysis
Parse k=, h=, a= tags and derive RSA key bit-length from the public key so consumers can detect weak configurations (SHA-1, short keys). Scoring now penalises rsa-sha1 (cap 60), RSA <1024 bit (cap 25), and RSA <2048 bit (cap 75); Ed25519 receives no penalty. Fixes: #37
This commit is contained in:
parent
369a13526f
commit
1b8627ef86
4 changed files with 321 additions and 69 deletions
|
|
@ -106,7 +106,7 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, headersResults *model.Head
|
|||
|
||||
// Check DKIM records by parsing DKIM-Signature headers directly
|
||||
for _, sig := range parseDKIMSignatures(email.Header["Dkim-Signature"]) {
|
||||
dkimRecord := d.checkDKIMRecord(sig.Domain, sig.Selector)
|
||||
dkimRecord := d.checkDKIMRecord(sig)
|
||||
if dkimRecord != nil {
|
||||
if results.DkimRecords == nil {
|
||||
results.DkimRecords = new([]model.DKIMRecord)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ package analyzer
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
|
@ -30,17 +32,18 @@ import (
|
|||
"git.happydns.org/happyDeliver/internal/utils"
|
||||
)
|
||||
|
||||
// DKIMHeader holds the domain and selector extracted from a DKIM-Signature header.
|
||||
// DKIMHeader holds the domain, selector and signing algorithm from a DKIM-Signature header.
|
||||
type DKIMHeader struct {
|
||||
Domain string
|
||||
Selector string
|
||||
Domain string
|
||||
Selector string
|
||||
Algorithm string // from a= tag (e.g. rsa-sha256, ed25519-sha256)
|
||||
}
|
||||
|
||||
// parseDKIMSignatures extracts domain and selector from DKIM-Signature header values.
|
||||
// parseDKIMSignatures extracts domain, selector and algorithm from DKIM-Signature header values.
|
||||
func parseDKIMSignatures(signatures []string) []DKIMHeader {
|
||||
var results []DKIMHeader
|
||||
for _, sig := range signatures {
|
||||
var domain, selector string
|
||||
var domain, selector, algorithm string
|
||||
for _, part := range strings.Split(sig, ";") {
|
||||
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
|
||||
if len(kv) != 2 {
|
||||
|
|
@ -53,19 +56,61 @@ func parseDKIMSignatures(signatures []string) []DKIMHeader {
|
|||
domain = val
|
||||
case "s":
|
||||
selector = val
|
||||
case "a":
|
||||
algorithm = val
|
||||
}
|
||||
}
|
||||
if domain != "" && selector != "" {
|
||||
results = append(results, DKIMHeader{Domain: domain, Selector: selector})
|
||||
results = append(results, DKIMHeader{Domain: domain, Selector: selector, Algorithm: algorithm})
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// checkmodel.DKIMRecord looks up and validates DKIM record for a domain and selector
|
||||
func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *model.DKIMRecord {
|
||||
// DKIM records are at: selector._domainkey.domain
|
||||
dkimDomain := fmt.Sprintf("%s._domainkey.%s", selector, domain)
|
||||
// parseDKIMTags splits a DKIM DNS record into a tag→value map.
|
||||
func parseDKIMTags(record string) map[string]string {
|
||||
tags := make(map[string]string)
|
||||
for _, part := range strings.Split(record, ";") {
|
||||
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
tags[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
// parseKeySize derives the public key bit length from a base64-encoded DER public key.
|
||||
// For RSA keys it parses the PKIX structure; for Ed25519 it always returns 256.
|
||||
func parseKeySize(keyType, p string) *int {
|
||||
switch strings.ToLower(keyType) {
|
||||
case "ed25519":
|
||||
return utils.PtrTo(256)
|
||||
case "rsa", "":
|
||||
der, err := base64.StdEncoding.DecodeString(p)
|
||||
if err != nil {
|
||||
// Try without padding
|
||||
der, err = base64.RawStdEncoding.DecodeString(p)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
pub, err := x509.ParsePKIXPublicKey(der)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if rsaPub, ok := pub.(interface{ Size() int }); ok {
|
||||
bits := rsaPub.Size() * 8
|
||||
return &bits
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkDKIMRecord looks up and validates DKIM record for a domain and selector.
|
||||
func (d *DNSAnalyzer) checkDKIMRecord(h DKIMHeader) *model.DKIMRecord {
|
||||
dkimDomain := fmt.Sprintf("%s._domainkey.%s", h.Selector, h.Domain)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), d.Timeout)
|
||||
defer cancel()
|
||||
|
|
@ -73,53 +118,83 @@ func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *model.DKIMRecord
|
|||
txtRecords, err := d.resolver.LookupTXT(ctx, dkimDomain)
|
||||
if err != nil {
|
||||
return &model.DKIMRecord{
|
||||
Selector: selector,
|
||||
Domain: domain,
|
||||
Valid: false,
|
||||
Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DKIM record: %v", err)),
|
||||
Selector: h.Selector,
|
||||
Domain: h.Domain,
|
||||
SigningAlgorithm: signingAlgorithmPtr(h.Algorithm),
|
||||
Valid: false,
|
||||
Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DKIM record: %v", err)),
|
||||
}
|
||||
}
|
||||
|
||||
if len(txtRecords) == 0 {
|
||||
return &model.DKIMRecord{
|
||||
Selector: selector,
|
||||
Domain: domain,
|
||||
Valid: false,
|
||||
Error: utils.PtrTo("No DKIM record found"),
|
||||
Selector: h.Selector,
|
||||
Domain: h.Domain,
|
||||
SigningAlgorithm: signingAlgorithmPtr(h.Algorithm),
|
||||
Valid: false,
|
||||
Error: utils.PtrTo("No DKIM record found"),
|
||||
}
|
||||
}
|
||||
|
||||
// Concatenate all TXT record parts (DKIM can be split)
|
||||
dkimRecord := strings.Join(txtRecords, "")
|
||||
|
||||
// Basic validation - should contain "v=DKIM1" and "p=" (public key)
|
||||
if !d.validateDKIM(dkimRecord) {
|
||||
return &model.DKIMRecord{
|
||||
Selector: selector,
|
||||
Domain: domain,
|
||||
Record: utils.PtrTo(dkimRecord),
|
||||
Valid: false,
|
||||
Error: utils.PtrTo("DKIM record appears malformed"),
|
||||
Selector: h.Selector,
|
||||
Domain: h.Domain,
|
||||
Record: utils.PtrTo(dkimRecord),
|
||||
SigningAlgorithm: signingAlgorithmPtr(h.Algorithm),
|
||||
Valid: false,
|
||||
Error: utils.PtrTo("DKIM record appears malformed"),
|
||||
}
|
||||
}
|
||||
|
||||
tags := parseDKIMTags(dkimRecord)
|
||||
|
||||
keyType := tags["k"]
|
||||
if keyType == "" {
|
||||
keyType = "rsa" // RFC 6376 default
|
||||
}
|
||||
|
||||
var hashAlgorithms []string
|
||||
if h, ok := tags["h"]; ok && h != "" {
|
||||
for _, alg := range strings.Split(h, ":") {
|
||||
if a := strings.TrimSpace(alg); a != "" {
|
||||
hashAlgorithms = append(hashAlgorithms, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
if hashAlgorithms == nil {
|
||||
hashAlgorithms = []string{}
|
||||
}
|
||||
|
||||
return &model.DKIMRecord{
|
||||
Selector: selector,
|
||||
Domain: domain,
|
||||
Record: &dkimRecord,
|
||||
Valid: true,
|
||||
Selector: h.Selector,
|
||||
Domain: h.Domain,
|
||||
Record: &dkimRecord,
|
||||
KeyType: utils.PtrTo(keyType),
|
||||
HashAlgorithms: &hashAlgorithms,
|
||||
SigningAlgorithm: signingAlgorithmPtr(h.Algorithm),
|
||||
KeySize: parseKeySize(keyType, tags["p"]),
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
// validateDKIM performs basic DKIM record validation
|
||||
func signingAlgorithmPtr(a string) *string {
|
||||
if a == "" {
|
||||
return nil
|
||||
}
|
||||
return &a
|
||||
}
|
||||
|
||||
// validateDKIM performs basic DKIM record validation.
|
||||
func (d *DNSAnalyzer) validateDKIM(record string) bool {
|
||||
// Should contain p= tag (public key)
|
||||
if !strings.Contains(record, "p=") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Often contains v=DKIM1 but not required
|
||||
// If v= is present, it should be DKIM1
|
||||
// If v= is present, it must be DKIM1
|
||||
if strings.Contains(record, "v=") && !strings.Contains(record, "v=DKIM1") {
|
||||
return false
|
||||
}
|
||||
|
|
@ -128,21 +203,57 @@ func (d *DNSAnalyzer) validateDKIM(record string) bool {
|
|||
}
|
||||
|
||||
func (d *DNSAnalyzer) calculateDKIMScore(results *model.DNSResults) (score int) {
|
||||
// DKIM provides strong email authentication
|
||||
if results.DkimRecords != nil && len(*results.DkimRecords) > 0 {
|
||||
hasValidDKIM := false
|
||||
for _, dkim := range *results.DkimRecords {
|
||||
if dkim.Valid {
|
||||
hasValidDKIM = true
|
||||
break
|
||||
if results.DkimRecords == nil || len(*results.DkimRecords) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
hasValid := false
|
||||
for _, dkim := range *results.DkimRecords {
|
||||
if dkim.Valid {
|
||||
hasValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasValid {
|
||||
return 25
|
||||
}
|
||||
|
||||
score = 100
|
||||
|
||||
// Apply security penalties on the best valid record
|
||||
for _, dkim := range *results.DkimRecords {
|
||||
if !dkim.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
// SHA-1 signing is deprecated (RFC 8301)
|
||||
if dkim.SigningAlgorithm != nil && strings.HasSuffix(*dkim.SigningAlgorithm, "-sha1") {
|
||||
if score > 60 {
|
||||
score = 60
|
||||
}
|
||||
}
|
||||
if hasValidDKIM {
|
||||
score += 100
|
||||
} else {
|
||||
// Partial credit if DKIM record exists but has issues
|
||||
score += 25
|
||||
|
||||
// Key size penalties apply only to RSA
|
||||
keyType := ""
|
||||
if dkim.KeyType != nil {
|
||||
keyType = strings.ToLower(*dkim.KeyType)
|
||||
}
|
||||
if keyType == "rsa" || keyType == "" {
|
||||
if dkim.KeySize != nil {
|
||||
switch {
|
||||
case *dkim.KeySize < 1024:
|
||||
if score > 25 {
|
||||
score = 25
|
||||
}
|
||||
case *dkim.KeySize < 2048:
|
||||
if score > 75 {
|
||||
score = 75
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ed25519 keys (256-bit curve, ~3000-bit RSA equivalent) need no penalty.
|
||||
}
|
||||
|
||||
return
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@
|
|||
package analyzer
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -47,56 +51,56 @@ func TestParseDKIMSignatures(t *testing.T) {
|
|||
signatures: []string{
|
||||
`v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:subject:date:message-id; bh=abcdef1234567890=; b=SIGNATURE_DATA_HERE==`,
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "gmail.com", Selector: "20210112"}},
|
||||
expected: []DKIMHeader{{Domain: "gmail.com", Selector: "20210112", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "Microsoft 365 style",
|
||||
signatures: []string{
|
||||
`v=1; a=rsa-sha256; c=relaxed/relaxed; d=contoso.com; s=selector1; h=From:Date:Subject:Message-ID; bh=UErATeHehIIPIXPeUA==; b=SIGNATURE_DATA==`,
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "contoso.com", Selector: "selector1"}},
|
||||
expected: []DKIMHeader{{Domain: "contoso.com", Selector: "selector1", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "Tab-folded multiline (Postfix-style)",
|
||||
signatures: []string{
|
||||
"v=1; a=rsa-sha256; c=relaxed/simple; d=nemunai.re; s=thot;\r\n\tt=1760866834; bh=YNB7c8Qgm8YGn9X1FAXTcdpO7t4YSZFiMrmpCfD/3zw=;\r\n\th=From:To:Subject;\r\n\tb=T4TFaypMpsHGYCl3PGLwmzOYRF11rYjC7lF8V5VFU+ldvG8WBpFn==",
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "nemunai.re", Selector: "thot"}},
|
||||
expected: []DKIMHeader{{Domain: "nemunai.re", Selector: "thot", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "Space-folded multiline (RFC-style)",
|
||||
signatures: []string{
|
||||
"v=1; a=rsa-sha256; c=relaxed/relaxed;\r\n d=football.example.com; i=@football.example.com;\r\n q=dns/txt; s=test; t=1528637909; h=from:to:subject;\r\n bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r\n b=F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8Gwps==",
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "football.example.com", Selector: "test"}},
|
||||
expected: []DKIMHeader{{Domain: "football.example.com", Selector: "test", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "d= and s= on separate continuation lines",
|
||||
signatures: []string{
|
||||
"v=1; a=rsa-sha256;\r\n\tc=relaxed/relaxed;\r\n\td=mycompany.com;\r\n\ts=selector1;\r\n\tbh=hash=;\r\n\tb=sig==",
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "mycompany.com", Selector: "selector1"}},
|
||||
expected: []DKIMHeader{{Domain: "mycompany.com", Selector: "selector1", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "No space after semicolons",
|
||||
signatures: []string{
|
||||
`v=1;a=rsa-sha256;c=relaxed/relaxed;d=example.net;s=mail;h=from:to:subject;bh=abc=;b=xyz==`,
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "example.net", Selector: "mail"}},
|
||||
expected: []DKIMHeader{{Domain: "example.net", Selector: "mail", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "Multiple spaces after semicolons",
|
||||
signatures: []string{
|
||||
`v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=myselector; bh=hash=; b=sig==`,
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "example.com", Selector: "myselector"}},
|
||||
expected: []DKIMHeader{{Domain: "example.com", Selector: "myselector", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "Ed25519 signature (RFC 8463)",
|
||||
signatures: []string{
|
||||
"v=1; a=ed25519-sha256; c=relaxed/relaxed;\r\n d=football.example.com; i=@football.example.com;\r\n q=dns/txt; s=brisbane; t=1528637909; h=from:to:subject;\r\n bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r\n b=/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQ==",
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "football.example.com", Selector: "brisbane"}},
|
||||
expected: []DKIMHeader{{Domain: "football.example.com", Selector: "brisbane", Algorithm: "ed25519-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "Multiple signatures (ESP double-signing)",
|
||||
|
|
@ -105,8 +109,8 @@ func TestParseDKIMSignatures(t *testing.T) {
|
|||
`v=1; a=rsa-sha256; c=relaxed/relaxed; d=sendib.com; s=mail; h=from:to:subject; bh=hash1=; b=sig2==`,
|
||||
},
|
||||
expected: []DKIMHeader{
|
||||
{Domain: "mydomain.com", Selector: "mail"},
|
||||
{Domain: "sendib.com", Selector: "mail"},
|
||||
{Domain: "mydomain.com", Selector: "mail", Algorithm: "rsa-sha256"},
|
||||
{Domain: "sendib.com", Selector: "mail", Algorithm: "rsa-sha256"},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -116,8 +120,8 @@ func TestParseDKIMSignatures(t *testing.T) {
|
|||
`v=1; a=rsa-sha256; c=relaxed/relaxed; d=football.example.com; s=test; h=from:to:subject; bh=hash=; b=rsaSig==`,
|
||||
},
|
||||
expected: []DKIMHeader{
|
||||
{Domain: "football.example.com", Selector: "brisbane"},
|
||||
{Domain: "football.example.com", Selector: "test"},
|
||||
{Domain: "football.example.com", Selector: "brisbane", Algorithm: "ed25519-sha256"},
|
||||
{Domain: "football.example.com", Selector: "test", Algorithm: "rsa-sha256"},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -127,8 +131,8 @@ func TestParseDKIMSignatures(t *testing.T) {
|
|||
`v=1; a=rsa-sha256; c=relaxed/simple; d=customerdomain.io; s=ug7nbtf4gccmlpwj322ax3p6ow6fovbt; h=from:to:subject; bh=sesHash=; b=customSig==`,
|
||||
},
|
||||
expected: []DKIMHeader{
|
||||
{Domain: "amazonses.com", Selector: "224i4yxa5dv7c2xz3womw6peuabd"},
|
||||
{Domain: "customerdomain.io", Selector: "ug7nbtf4gccmlpwj322ax3p6ow6fovbt"},
|
||||
{Domain: "amazonses.com", Selector: "224i4yxa5dv7c2xz3womw6peuabd", Algorithm: "rsa-sha256"},
|
||||
{Domain: "customerdomain.io", Selector: "ug7nbtf4gccmlpwj322ax3p6ow6fovbt", Algorithm: "rsa-sha256"},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -136,56 +140,56 @@ func TestParseDKIMSignatures(t *testing.T) {
|
|||
signatures: []string{
|
||||
`v=1; a=rsa-sha256; c=relaxed/relaxed; d=mail.example.co.uk; s=dkim2025; h=from:to:subject; bh=hash=; b=sig==`,
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "mail.example.co.uk", Selector: "dkim2025"}},
|
||||
expected: []DKIMHeader{{Domain: "mail.example.co.uk", Selector: "dkim2025", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "Deeply nested subdomain",
|
||||
signatures: []string{
|
||||
`v=1; a=rsa-sha256; c=relaxed/relaxed; d=bounce.transactional.mail.example.com; s=s2048; h=from:to:subject; bh=hash=; b=sig==`,
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "bounce.transactional.mail.example.com", Selector: "s2048"}},
|
||||
expected: []DKIMHeader{{Domain: "bounce.transactional.mail.example.com", Selector: "s2048", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "Selector with hyphens (Microsoft 365 custom domain style)",
|
||||
signatures: []string{
|
||||
`v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=selector1-contoso-com; h=from:to:subject; bh=hash=; b=sig==`,
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1-contoso-com"}},
|
||||
expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1-contoso-com", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "Selector with dots",
|
||||
signatures: []string{
|
||||
`v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=smtp.mail; h=from:to:subject; bh=hash=; b=sig==`,
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "example.com", Selector: "smtp.mail"}},
|
||||
expected: []DKIMHeader{{Domain: "example.com", Selector: "smtp.mail", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "Single-character selector",
|
||||
signatures: []string{
|
||||
`v=1; a=rsa-sha256; c=relaxed/relaxed; d=tiny.io; s=x; h=from:to:subject; bh=hash=; b=sig==`,
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "tiny.io", Selector: "x"}},
|
||||
expected: []DKIMHeader{{Domain: "tiny.io", Selector: "x", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "Postmark-style timestamp selector, s= before d=",
|
||||
signatures: []string{
|
||||
`v=1; a=rsa-sha1; c=relaxed/relaxed; s=20130519032151pm; d=postmarkapp.com; h=From:Date:Subject; bh=vYFvy46eesUDGJ45hyBTH30JfN4=; b=iHeFQ+7rCiSQs3DPjR2eUSZSv4i==`,
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "postmarkapp.com", Selector: "20130519032151pm"}},
|
||||
expected: []DKIMHeader{{Domain: "postmarkapp.com", Selector: "20130519032151pm", Algorithm: "rsa-sha1"}},
|
||||
},
|
||||
{
|
||||
name: "d= and s= at the very end",
|
||||
signatures: []string{
|
||||
`v=1; a=rsa-sha256; c=relaxed/relaxed; h=from:to:subject; bh=hash=; b=sig==; d=example.net; s=trailing`,
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "example.net", Selector: "trailing"}},
|
||||
expected: []DKIMHeader{{Domain: "example.net", Selector: "trailing", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "Full tag set",
|
||||
signatures: []string{
|
||||
`v=1; a=rsa-sha256; d=example.com; s=selector1; c=relaxed/simple; q=dns/txt; i=user@example.com; t=1255993973; x=1256598773; h=From:Sender:Reply-To:Subject:Date:Message-Id:To:Cc; bh=+7qxGePcmmrtZAIVQAtkSSGHfQ/ftNuvUTWJ3vXC9Zc=; b=dB85+qM+If1KGQmqMLNpqLgNtUaG5dhGjYjQD6/QXtXmViJx8tf9gLEjcHr+musLCAvr0Fsn1DA3ZLLlUxpf4AR==`,
|
||||
},
|
||||
expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1"}},
|
||||
expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1", Algorithm: "rsa-sha256"}},
|
||||
},
|
||||
{
|
||||
name: "Missing d= tag",
|
||||
|
|
@ -216,8 +220,8 @@ func TestParseDKIMSignatures(t *testing.T) {
|
|||
`v=1; a=rsa-sha256; c=relaxed/relaxed; d=also-good.com; s=sel2; h=from:to; bh=hash=; b=sig==`,
|
||||
},
|
||||
expected: []DKIMHeader{
|
||||
{Domain: "good.com", Selector: "sel1"},
|
||||
{Domain: "also-good.com", Selector: "sel2"},
|
||||
{Domain: "good.com", Selector: "sel1", Algorithm: "rsa-sha256"},
|
||||
{Domain: "also-good.com", Selector: "sel2", Algorithm: "rsa-sha256"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -235,6 +239,9 @@ func TestParseDKIMSignatures(t *testing.T) {
|
|||
if result[i].Selector != tt.expected[i].Selector {
|
||||
t.Errorf("result[%d].Selector = %q, want %q", i, result[i].Selector, tt.expected[i].Selector)
|
||||
}
|
||||
if result[i].Algorithm != tt.expected[i].Algorithm {
|
||||
t.Errorf("result[%d].Algorithm = %q, want %q", i, result[i].Algorithm, tt.expected[i].Algorithm)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -284,3 +291,119 @@ func TestValidateDKIM(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDKIMTags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
record string
|
||||
wantTags map[string]string
|
||||
}{
|
||||
{
|
||||
name: "standard RSA record",
|
||||
record: "v=DKIM1; k=rsa; p=MIIBI; h=sha256",
|
||||
wantTags: map[string]string{"v": "DKIM1", "k": "rsa", "p": "MIIBI", "h": "sha256"},
|
||||
},
|
||||
{
|
||||
name: "ed25519 record",
|
||||
record: "v=DKIM1; k=ed25519; p=11qYAYKxCrfVS",
|
||||
wantTags: map[string]string{"v": "DKIM1", "k": "ed25519", "p": "11qYAYKxCrfVS"},
|
||||
},
|
||||
{
|
||||
name: "missing k= defaults",
|
||||
record: "v=DKIM1; p=MIIBI",
|
||||
wantTags: map[string]string{"v": "DKIM1", "p": "MIIBI"},
|
||||
},
|
||||
{
|
||||
name: "empty record",
|
||||
record: "",
|
||||
wantTags: map[string]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseDKIMTags(tt.record)
|
||||
for key, want := range tt.wantTags {
|
||||
if got[key] != want {
|
||||
t.Errorf("tag %q = %q, want %q", key, got[key], want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseKeySize(t *testing.T) {
|
||||
// Generate a real RSA key for testing
|
||||
rsaKey1024, _ := rsa.GenerateKey(rand.Reader, 1024)
|
||||
rsaKey2048, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
|
||||
der1024, _ := x509.MarshalPKIXPublicKey(&rsaKey1024.PublicKey)
|
||||
der2048, _ := x509.MarshalPKIXPublicKey(&rsaKey2048.PublicKey)
|
||||
|
||||
p1024 := base64.StdEncoding.EncodeToString(der1024)
|
||||
p2048 := base64.StdEncoding.EncodeToString(der2048)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
keyType string
|
||||
p string
|
||||
want *int
|
||||
}{
|
||||
{
|
||||
name: "RSA 1024",
|
||||
keyType: "rsa",
|
||||
p: p1024,
|
||||
want: intPtr(1024),
|
||||
},
|
||||
{
|
||||
name: "RSA 2048",
|
||||
keyType: "rsa",
|
||||
p: p2048,
|
||||
want: intPtr(2048),
|
||||
},
|
||||
{
|
||||
name: "Ed25519 always 256",
|
||||
keyType: "ed25519",
|
||||
p: "11qYAYKxCrfVS",
|
||||
want: intPtr(256),
|
||||
},
|
||||
{
|
||||
name: "Unknown key type",
|
||||
keyType: "unknown",
|
||||
p: "somedata",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "Invalid RSA base64",
|
||||
keyType: "rsa",
|
||||
p: "!!!not-base64!!!",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "Empty k= defaults to RSA",
|
||||
keyType: "",
|
||||
p: p2048,
|
||||
want: intPtr(2048),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseKeySize(tt.keyType, tt.p)
|
||||
if tt.want == nil {
|
||||
if got != nil {
|
||||
t.Errorf("parseKeySize(%q, ...) = %d, want nil", tt.keyType, *got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatalf("parseKeySize(%q, ...) = nil, want %d", tt.keyType, *tt.want)
|
||||
}
|
||||
if *got != *tt.want {
|
||||
t.Errorf("parseKeySize(%q, ...) = %d, want %d", tt.keyType, *got, *tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func intPtr(v int) *int { return &v }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue