Skip to content

Commit fb0bf24

Browse files
committed
Optimize MessageCanonicalization parsing logic
1 parent c3e9e0f commit fb0bf24

File tree

6 files changed

+207
-11
lines changed

6 files changed

+207
-11
lines changed

src/Nager.EmailAuthentication.UnitTest/DkimSignatureParserTests/BasicTest.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ public void TryParse_ValidDkimSignature_ReturnsTrueAndDkimSignature()
3232
Assert.IsNotNull(dkimSignature);
3333
Assert.AreEqual("1", dkimSignature.Version);
3434
Assert.AreEqual(SignatureAlgorithm.RsaSha256, dkimSignature.SignatureAlgorithm);
35-
Assert.AreEqual("relaxed/simple", dkimSignature.MessageCanonicalization);
35+
Assert.AreEqual(CanonicalizationType.Relaxed, dkimSignature.MessageCanonicalizationHeader);
36+
Assert.AreEqual(CanonicalizationType.Simple, dkimSignature.MessageCanonicalizationBody);
3637
Assert.AreEqual("dns/txt", dkimSignature.QueryMethods);
3738
Assert.AreEqual("domain.com", dkimSignature.SigningDomainIdentifier);
3839
Assert.AreEqual("[email protected]", dkimSignature.AgentOrUserIdentifier);

src/Nager.EmailAuthentication.UnitTest/DkimSignatureParserTests/FoldingTest.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public void TryParse_ValidSelector_ReturnsTrueAndPopulatesDataFragment()
1717

1818
Assert.AreEqual("1", dkimSignature.Version);
1919
Assert.AreEqual(SignatureAlgorithm.RsaSha256, dkimSignature.SignatureAlgorithm);
20-
Assert.AreEqual("relaxed/relaxed", dkimSignature.MessageCanonicalization);
20+
Assert.AreEqual(CanonicalizationType.Relaxed, dkimSignature.MessageCanonicalizationHeader);
21+
Assert.AreEqual(CanonicalizationType.Relaxed, dkimSignature.MessageCanonicalizationBody);
2122
Assert.AreEqual("dmarc.com", dkimSignature.SigningDomainIdentifier);
2223
Assert.AreEqual("selector1", dkimSignature.Selector);
2324
//Assert.AreEqual(["cc"], dkimSignature.SignedHeaderFields);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using Nager.EmailAuthentication.Models;
2+
3+
namespace Nager.EmailAuthentication.UnitTest.DkimSignatureParserTests
4+
{
5+
[TestClass]
6+
public sealed class MessageCanonicalizationTest
7+
{
8+
[TestMethod]
9+
public void TryParse_NoMessageCanonicalization_ReturnsTrueAndPopulatesDkimSignature()
10+
{
11+
var dkimSignatureRaw = "v=1; a=rsa-sha256; d=domain.com; s=test; h=message-id:from; bh=testbodyhash=; b=signaturedata";
12+
13+
var isSuccessful = DkimSignatureParser.TryParse(dkimSignatureRaw, out var dkimSignature, out var parsingResults);
14+
15+
Assert.IsTrue(isSuccessful);
16+
Assert.IsNotNull(dkimSignature);
17+
Assert.IsNull(parsingResults, "ParsingResults is not null");
18+
}
19+
20+
[TestMethod]
21+
public void TryParse_SingleMessageCanonicalization1_ReturnsTrueAndPopulatesDkimSignature()
22+
{
23+
var dkimSignatureRaw = "v=1; a=rsa-sha256; d=domain.com; c=relaxed; s=test; h=message-id:from; bh=testbodyhash=; b=signaturedata";
24+
25+
var isSuccessful = DkimSignatureParser.TryParse(dkimSignatureRaw, out var dkimSignature, out var parsingResults);
26+
27+
Assert.IsTrue(isSuccessful);
28+
Assert.IsNotNull(dkimSignature);
29+
Assert.IsNull(parsingResults, "ParsingResults is not null");
30+
Assert.AreEqual(CanonicalizationType.Relaxed, dkimSignature.MessageCanonicalizationHeader);
31+
Assert.AreEqual(CanonicalizationType.Simple, dkimSignature.MessageCanonicalizationBody);
32+
}
33+
34+
35+
[TestMethod]
36+
public void TryParse_SingleMessageCanonicalization2_ReturnsTrueAndPopulatesDkimSignature()
37+
{
38+
var dkimSignatureRaw = "v=1; a=rsa-sha256; d=domain.com; c=simple; s=test; h=message-id:from; bh=testbodyhash=; b=signaturedata";
39+
40+
var isSuccessful = DkimSignatureParser.TryParse(dkimSignatureRaw, out var dkimSignature, out var parsingResults);
41+
42+
Assert.IsTrue(isSuccessful);
43+
Assert.IsNotNull(dkimSignature);
44+
Assert.IsNull(parsingResults, "ParsingResults is not null");
45+
Assert.AreEqual(CanonicalizationType.Simple, dkimSignature.MessageCanonicalizationHeader);
46+
Assert.AreEqual(CanonicalizationType.Simple, dkimSignature.MessageCanonicalizationBody);
47+
}
48+
49+
[TestMethod]
50+
public void TryParse_DefaultMessageCanonicalization1_ReturnsTrueAndPopulatesDkimSignature()
51+
{
52+
var dkimSignatureRaw = "v=1; a=rsa-sha256; d=domain.com; c=simple/simple; s=test; h=message-id:from; bh=testbodyhash=; b=signaturedata";
53+
54+
var isSuccessful = DkimSignatureParser.TryParse(dkimSignatureRaw, out var dkimSignature, out var parsingResults);
55+
56+
Assert.IsTrue(isSuccessful);
57+
Assert.IsNotNull(dkimSignature);
58+
Assert.IsNull(parsingResults, "ParsingResults is not null");
59+
Assert.AreEqual(CanonicalizationType.Simple, dkimSignature.MessageCanonicalizationHeader);
60+
Assert.AreEqual(CanonicalizationType.Simple, dkimSignature.MessageCanonicalizationBody);
61+
}
62+
63+
[TestMethod]
64+
public void TryParse_DefaultMessageCanonicalization2_ReturnsTrueAndPopulatesDkimSignature()
65+
{
66+
var dkimSignatureRaw = "v=1; a=rsa-sha256; d=domain.com; c=simple/relaxed; s=test; h=message-id:from; bh=testbodyhash=; b=signaturedata";
67+
68+
var isSuccessful = DkimSignatureParser.TryParse(dkimSignatureRaw, out var dkimSignature, out var parsingResults);
69+
70+
Assert.IsTrue(isSuccessful);
71+
Assert.IsNotNull(dkimSignature);
72+
Assert.IsNull(parsingResults, "ParsingResults is not null");
73+
Assert.AreEqual(CanonicalizationType.Simple, dkimSignature.MessageCanonicalizationHeader);
74+
Assert.AreEqual(CanonicalizationType.Relaxed, dkimSignature.MessageCanonicalizationBody);
75+
}
76+
77+
[TestMethod]
78+
public void TryParse_DefaultMessageCanonicalization3_ReturnsTrueAndPopulatesDkimSignature()
79+
{
80+
var dkimSignatureRaw = "v=1; a=rsa-sha256; d=domain.com; c=relaxed/relaxed; s=test; h=message-id:from; bh=testbodyhash=; b=signaturedata";
81+
82+
var isSuccessful = DkimSignatureParser.TryParse(dkimSignatureRaw, out var dkimSignature, out var parsingResults);
83+
84+
Assert.IsTrue(isSuccessful);
85+
Assert.IsNotNull(dkimSignature);
86+
Assert.IsNull(parsingResults, "ParsingResults is not null");
87+
Assert.AreEqual(CanonicalizationType.Relaxed, dkimSignature.MessageCanonicalizationHeader);
88+
Assert.AreEqual(CanonicalizationType.Relaxed, dkimSignature.MessageCanonicalizationBody);
89+
}
90+
91+
[TestMethod]
92+
public void TryParse_InvalidMessageCanonicalization1_ReturnsFalse()
93+
{
94+
var dkimSignatureRaw = "v=1; a=rsa-sha256; d=domain.com; c=test/test; s=test; h=message-id:from; bh=testbodyhash=; b=signaturedata";
95+
96+
var isSuccessful = DkimSignatureParser.TryParse(dkimSignatureRaw, out var dkimSignature, out var parsingResults);
97+
98+
Assert.IsFalse(isSuccessful);
99+
Assert.IsNull(dkimSignature);
100+
Assert.IsNull(parsingResults, "ParsingResults is not null");
101+
}
102+
103+
[TestMethod]
104+
public void TryParse_InvalidMessageCanonicalization2_ReturnsFalse()
105+
{
106+
var dkimSignatureRaw = "v=1; a=rsa-sha256; d=domain.com; c=relaxed/simple/test; s=test; h=message-id:from; bh=testbodyhash=; b=signaturedata";
107+
108+
var isSuccessful = DkimSignatureParser.TryParse(dkimSignatureRaw, out var dkimSignature, out var parsingResults);
109+
110+
Assert.IsFalse(isSuccessful);
111+
Assert.IsNull(dkimSignature);
112+
Assert.IsNull(parsingResults, "ParsingResults is not null");
113+
}
114+
}
115+
}

src/Nager.EmailAuthentication/DkimSignatureParser.cs

+59-7
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,50 @@ public static bool TryParse(
9595
return false;
9696
}
9797

98-
if (string.IsNullOrEmpty(dkimSignatureDataFragment.MessageCanonicalization))
98+
if (!TryGetSignatureAlgorithm(dkimSignatureDataFragment.SignatureAlgorithm, out var signatureAlgorithm))
9999
{
100100
return false;
101101
}
102102

103-
if (!TryGetSignatureAlgorithm(dkimSignatureDataFragment.SignatureAlgorithm, out var signatureAlgorithm))
103+
_ = TryParseUnixTimestamp(dkimSignatureDataFragment.Timestamp, out var timestamp);
104+
_ = TryParseUnixTimestamp(dkimSignatureDataFragment.SignatureExpiration, out var signatureExpiration);
105+
106+
var messageCanonicalizationHeader = CanonicalizationType.Simple;
107+
var messageCanonicalizationBody = CanonicalizationType.Simple;
108+
109+
if (!string.IsNullOrEmpty(dkimSignatureDataFragment.MessageCanonicalization))
104110
{
105-
return false;
106-
}
111+
var parts = dkimSignatureDataFragment.MessageCanonicalization.Split('/');
112+
113+
if (parts.Length >= 1)
114+
{
115+
if (TryGetCanonicalizationType(parts[0], out var canonicalizationType))
116+
{
117+
messageCanonicalizationHeader = canonicalizationType.Value;
118+
}
119+
else
120+
{
121+
return false;
122+
}
123+
}
107124

108-
TryParseUnixTimestamp(dkimSignatureDataFragment.Timestamp, out var timestamp);
109-
TryParseUnixTimestamp(dkimSignatureDataFragment.SignatureExpiration, out var signatureExpiration);
125+
if (parts.Length == 2)
126+
{
127+
if (TryGetCanonicalizationType(parts[1], out var canonicalizationType))
128+
{
129+
messageCanonicalizationBody = canonicalizationType.Value;
130+
}
131+
else
132+
{
133+
return false;
134+
}
135+
}
136+
137+
if (parts.Length > 2)
138+
{
139+
return false;
140+
}
141+
}
110142

111143
dkimSignature = new DkimSignature
112144
{
@@ -120,13 +152,33 @@ public static bool TryParse(
120152
SignatureExpiration = signatureExpiration,
121153
AgentOrUserIdentifier = dkimSignatureDataFragment.AgentOrUserIdentifier,
122154
SignedHeaderFields = dkimSignatureDataFragment.SignedHeaderFields.Split(':'),
123-
MessageCanonicalization = dkimSignatureDataFragment.MessageCanonicalization,
155+
MessageCanonicalizationHeader = messageCanonicalizationHeader,
156+
MessageCanonicalizationBody = messageCanonicalizationBody,
124157
Timestamp = timestamp
125158
};
126159

127160
return true;
128161
}
129162

163+
private static bool TryGetCanonicalizationType(
164+
string canonicalizationTypeRaw,
165+
[NotNullWhen(true)] out CanonicalizationType? canonicalizationType)
166+
{
167+
if (canonicalizationTypeRaw.Equals("relaxed", StringComparison.OrdinalIgnoreCase))
168+
{
169+
canonicalizationType = CanonicalizationType.Relaxed;
170+
return true;
171+
}
172+
else if (canonicalizationTypeRaw.Equals("simple", StringComparison.OrdinalIgnoreCase))
173+
{
174+
canonicalizationType = CanonicalizationType.Simple;
175+
return true;
176+
}
177+
178+
canonicalizationType = null;
179+
return false;
180+
}
181+
130182
private static bool TryGetSignatureAlgorithm(
131183
string? signatureAlgorithmRaw,
132184
[NotNullWhen(true)] out SignatureAlgorithm? signatureAlgorithm)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Nager.EmailAuthentication.Models
2+
{
3+
/// <summary>
4+
/// Defines the types of canonicalization used in DKIM (DomainKeys Identified Mail).
5+
/// Canonicalization determines how email headers and body content are normalized before signing.
6+
/// </summary>
7+
public enum CanonicalizationType
8+
{
9+
/// <summary>
10+
/// The "Simple" canonicalization applies minimal transformations,
11+
/// preserving whitespace and line breaks as they appear in the original message.
12+
/// </summary>
13+
Simple,
14+
15+
/// <summary>
16+
/// The "Relaxed" canonicalization applies normalization,
17+
/// such as collapsing whitespace and standardizing header field names.
18+
/// This makes the signature more tolerant to minor formatting changes.
19+
/// </summary>
20+
Relaxed
21+
}
22+
}

src/Nager.EmailAuthentication/Models/DkimSignature.cs

+7-2
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,14 @@ public class DkimSignature
2626
public required string BodyHash { get; set; }
2727

2828
/// <summary>
29-
/// Message canonicalization <strong>(c=)</strong>
29+
/// Header Message canonicalization <strong>(c=)</strong>
3030
/// </summary>
31-
public required string MessageCanonicalization { get; set; }
31+
public required CanonicalizationType MessageCanonicalizationHeader { get; set; }
32+
33+
/// <summary>
34+
/// Body Message canonicalization <strong>(c=)</strong>
35+
/// </summary>
36+
public required CanonicalizationType MessageCanonicalizationBody { get; set; }
3237

3338
/// <summary>
3439
/// Signing Domain Identifier <strong>(d=)</strong>

0 commit comments

Comments
 (0)