Code
·
137 lines
·
4335 bytes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137using System.Net;
using System.Security.Cryptography;
using System.Text;
namespace MailSharp.SMTP.Services;
public class DkimVerifier(IConfiguration configuration)
{
private static readonly string[] separator = ["\r\n", "\n"];
// Verify DKIM signature of incoming email
public async Task<bool> VerifyDkimAsync(string emlContent, string clientIp)
{
// Split headers and body
int headerEnd = emlContent.IndexOf("\r\n\r\n", StringComparison.Ordinal);
if (headerEnd == -1)
{
return false; // Invalid email format
}
string headers = emlContent[..headerEnd];
string body = emlContent[(headerEnd + 4)..];
// Extract DKIM-Signature header
string[] headerLines = headers.Split("\r\n");
string? dkimHeader = headerLines.FirstOrDefault(h => h.StartsWith("DKIM-Signature:", StringComparison.OrdinalIgnoreCase));
if (dkimHeader == null)
{
return false; // No DKIM signature
}
// Parse DKIM-Signature fields
var dkimFields = ParseDkimHeader(dkimHeader);
if (!dkimFields.TryGetValue("d", out string? domain) ||
!dkimFields.TryGetValue("s", out string? selector) ||
!dkimFields.TryGetValue("b", out string? signatureBase64) ||
!dkimFields.TryGetValue("bh", out string? bodyHashBase64) ||
!dkimFields.TryGetValue("h", out string? signedHeaders))
{
return false; // Invalid DKIM header
}
// Verify body hash
string canonicalBody = CanonicalizeBody(body);
byte[] bodyBytes = Encoding.ASCII.GetBytes(canonicalBody);
byte[] computedBodyHash = SHA256.HashData(bodyBytes);
string computedBodyHashBase64 = Convert.ToBase64String(computedBodyHash);
if (computedBodyHashBase64 != bodyHashBase64)
{
return false; // Body hash mismatch
}
// Fetch public key from DNS
string? publicKey = await FetchDkimPublicKeyAsync(selector, domain);
if (publicKey == null)
{
return false; // Public key not found
}
// Prepare headers for verification
string[] signedHeaderNames = [.. signedHeaders.Split(':').Select(h => h.Trim().ToLower())];
var headersToVerify = headerLines
.Where(h => signedHeaderNames.Any(sh => h.StartsWith(sh + ":", StringComparison.OrdinalIgnoreCase)))
.Reverse()
.ToList();
headersToVerify.Add(dkimHeader[..dkimHeader.IndexOf("; b=")]); // Include DKIM-Signature without signature
string canonicalHeaders = string.Join("\r\n", headersToVerify);
// Verify signature
try
{
using RSA rsa = RSA.Create();
rsa.ImportFromPem(publicKey);
byte[] signature = Convert.FromBase64String(signatureBase64);
byte[] headerBytes = Encoding.ASCII.GetBytes(canonicalHeaders);
return rsa.VerifyData(headerBytes, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
catch
{
return false; // Signature verification failed
}
}
// Canonicalize body (simple canonicalization per RFC 6376)
private static string CanonicalizeBody(string body)
{
body = body.TrimEnd();
body = string.Join("\r\n", body.Split(separator, StringSplitOptions.None));
return body + "\r\n";
}
// Parse DKIM-Signature header into key-value pairs
private static Dictionary<string, string> ParseDkimHeader(string dkimHeader)
{
var fields = new Dictionary<string, string>();
string[] parts = dkimHeader["DKIM-Signature:".Length..].Split(';');
foreach (string part in parts)
{
string trimmed = part.Trim();
if (string.IsNullOrEmpty(trimmed))
continue;
int equalsIndex = trimmed.IndexOf('=');
if (equalsIndex == -1)
continue;
string key = trimmed[..equalsIndex].Trim();
string value = trimmed[(equalsIndex + 1)..].Trim();
fields[key] = value;
}
return fields;
}
// Fetch DKIM public key from DNS TXT record
private static async Task<string?> FetchDkimPublicKeyAsync(string selector, string domain)
{
try
{
string query = $"{selector}._domainkey.{domain}";
var result = await Dns.GetHostEntryAsync(query);
string[] txtRecords = [.. result.Aliases
.Select(a => Dns.GetHostEntry(a).HostName)
.Where(h => h.Contains("p="))];
string? txtRecord = txtRecords.FirstOrDefault();
if (txtRecord == null)
return null;
int keyStart = txtRecord.IndexOf("p=") + 2;
string keyBase64 = txtRecord[keyStart..];
byte[] keyBytes = Convert.FromBase64String(keyBase64);
return Encoding.ASCII.GetString(keyBytes);
}
catch
{
return null;
}
}
}