MailSharp / MailSharp.SMTP / Services / DkimSigner.cs
Code · 76 lines · 2809 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
76using System.Security.Cryptography;
using System.Text;

namespace MailSharp.SMTP.Services;

public class DkimSigner(IConfiguration configuration)
{
	private static readonly string[] separator = ["\r\n", "\n"];

	// Sign email content with DKIM
	public string SignEmail(string emlContent, string selector, string domain)
	{
		string privateKey = configuration[$"SmtpSettings:Dkim:{domain}:PrivateKey"]
			?? throw new InvalidOperationException($"DKIM private key for {domain} not configured");

		// Parse email headers and body
		int headerEnd = emlContent.IndexOf("\r\n\r\n", StringComparison.Ordinal);
		if (headerEnd == -1)
		{
			throw new InvalidOperationException("Invalid email format: no header-body separator");
		}

		string headers = emlContent[..headerEnd];
		string body = emlContent[(headerEnd + 4)..];

		// Calculate body hash (BHASH)
		string canonicalBody = CanonicalizeBody(body);
		byte[] bodyBytes = Encoding.ASCII.GetBytes(canonicalBody);
		byte[] bodyHash = SHA256.HashData(bodyBytes);
		string bodyHashBase64 = Convert.ToBase64String(bodyHash);

		// Prepare DKIM-Signature header
		string dkimHeader = GenerateDkimHeader(selector, domain, bodyHashBase64, headers);
		byte[] headerBytes = Encoding.ASCII.GetBytes(dkimHeader);

		// Sign the DKIM-Signature header
		using RSA rsa = RSA.Create();
		rsa.ImportFromPem(privateKey);
		byte[] signature = rsa.SignData(headerBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
		string signatureBase64 = Convert.ToBase64String(signature);

		// Append signature to DKIM-Signature header
		dkimHeader += $"; s={signatureBase64}";

		// Insert DKIM-Signature header at the start of headers
		return $"{dkimHeader}\r\n{emlContent}";
	}

	// Canonicalize body (simple canonicalization per RFC 6376)
	private static string CanonicalizeBody(string body)
	{
		// Remove trailing empty lines and normalize line endings
		body = body.TrimEnd();
		body = string.Join("\r\n", body.Split(separator, StringSplitOptions.None));
		return body + "\r\n";
	}

	// Generate DKIM-Signature header
	private static string GenerateDkimHeader(string selector, string domain, string bodyHash, string headers)
	{
		string[] headerLines = headers.Split("\r\n");
		var signedHeaders = new[] { "from", "to", "subject", "date", "message-id" };
		var headersToSign = headerLines
			.Where(h => signedHeaders.Any(sh => h.StartsWith(sh + ":", StringComparison.OrdinalIgnoreCase)))
			.Select(h => h.ToLower())
			.Reverse()
			.ToList();

		string hValue = string.Join(":", signedHeaders);
		string canonicalHeaders = string.Join("\r\n", headersToSign);

		return $"DKIM-Signature: v=1; a=rsa-sha256; d={domain}; s={selector}; " +
			   $"c=simple/simple; q=dns/txt; t={DateTimeOffset.UtcNow.ToUnixTimeSeconds()}; " +
			   $"bh={bodyHash}; h={hValue}; b=";
	}
}