Skip to content

Commit 38d11ef

Browse files
committed
Add unit tests and doc strings to TryParseDistinguishedName method
1 parent 1d33eb8 commit 38d11ef

File tree

2 files changed

+177
-9
lines changed

2 files changed

+177
-9
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using AdvancedSystems.Security.Cryptography;
2+
using AdvancedSystems.Security.Extensions;
3+
4+
using Xunit;
5+
6+
namespace AdvancedSystems.Security.Tests.Extensions;
7+
8+
/// <summary>
9+
/// Tests the public methods in <seealso cref="CertificateExtensions"/>.
10+
/// </summary>
11+
public sealed class CertificateExtensionsTests
12+
{
13+
#region Tests
14+
15+
/// <summary>
16+
/// Tests that <seealso cref="CertificateExtensions.TryParseDistinguishedName(string, out DistinguishedName?)"/>
17+
/// extracts the RDNs from the DN correctly from the string.
18+
/// </summary>
19+
[Fact]
20+
public void TestTryParseDistinguishedName()
21+
{
22+
// Arrange
23+
string commonName = "Advanced Systems Root";
24+
string organizationalUnit = "R&D Department";
25+
string organization = "Advanced Systems Inc.";
26+
string locality = "Berlin";
27+
string state = "Berlin";
28+
string country = "DE";
29+
string distinguiedName = $"CN={commonName}, OU={organizationalUnit}, O={organization}, L={locality}, S={state}, C={country}";
30+
31+
// Act
32+
bool isDn = CertificateExtensions.TryParseDistinguishedName(distinguiedName, out DistinguishedName? dn);
33+
34+
// Assert
35+
Assert.Multiple(() =>
36+
{
37+
Assert.True(isDn);
38+
Assert.NotNull(dn);
39+
Assert.Equal(dn.CommonName, commonName);
40+
Assert.Equal(dn.OrganizationalUnit, organizationalUnit);
41+
Assert.Equal(dn.Organization, organization);
42+
Assert.Equal(dn.Locality, locality);
43+
Assert.Equal(dn.State, state);
44+
Assert.Equal(dn.Country, country);
45+
});
46+
}
47+
48+
/// <summary>
49+
/// Tests that <seealso cref="CertificateExtensions.TryParseDistinguishedName(string, out DistinguishedName?)"/>
50+
/// when the DN is malformed.
51+
/// </summary>
52+
[Fact]
53+
public void TestTryParseDistinguishedName_ReturnsNull()
54+
{
55+
// Arrange
56+
string distinguishedName = string.Empty;
57+
58+
// Act
59+
bool isDn = CertificateExtensions.TryParseDistinguishedName(distinguishedName, out DistinguishedName? dn);
60+
61+
// Assert
62+
Assert.False(isDn);
63+
Assert.Null(dn);
64+
}
65+
66+
#endregion
67+
}

AdvancedSystems.Security/Extensions/CertificateExtensions.cs

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using AdvancedSystems.Security.Abstractions.Exceptions;
88
using AdvancedSystems.Security.Cryptography;
99

10+
using static System.Net.WebRequestMethods;
11+
1012
namespace AdvancedSystems.Security.Extensions;
1113

1214
/// <summary>
@@ -46,7 +48,97 @@ public static X509Certificate2 GetCertificate<T>(this T store, string thumbprint
4648
?? throw new CertificateNotFoundException("No valid certificate matching the search criteria could be found in the store.");
4749
}
4850

49-
public static DistinguishedName ParseDistinguishedName(string distinguishedName)
51+
/// <summary>
52+
/// Attempts to parse the specified distinguished name (DN) string into a <see cref="DistinguishedName"/> object.
53+
/// </summary>
54+
/// <param name="distinguishedName">
55+
/// The DN string to parse, typically in the X.500 or LDAP DN format.
56+
/// </param>
57+
/// <param name="result">
58+
/// When this method returns, contains the parsed <see cref="DistinguishedName"/> object if the parsing was successful;
59+
/// otherwise, <see langword="null"/>.
60+
/// </param>
61+
/// <returns>
62+
/// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
63+
/// </returns>
64+
/// <remarks>
65+
/// The X.500 Distinguished Name (DN) and the LDAP Distinguished Name (DN) differ in syntax and conventions.
66+
/// <list type="table">
67+
/// <listheader>
68+
/// <term>
69+
/// X.500 Format
70+
/// </term>
71+
/// <description>
72+
/// Comes from the X.500 standard for directory services
73+
/// </description>
74+
/// </listheader>
75+
/// <item>
76+
/// <term>
77+
/// Separator
78+
/// </term>
79+
/// <description>
80+
/// Components are separated by commas (<c>,</c>).
81+
/// </description>
82+
/// </item>
83+
/// <item>
84+
/// <term>
85+
/// Order
86+
/// </term>
87+
/// <description>
88+
/// Attributes are typically listed in the most significant to least significant order
89+
/// (<i>root</i> to <i>leaf</i>).
90+
/// </description>
91+
/// </item>
92+
/// <item>
93+
/// <term>
94+
/// Attributes
95+
/// </term>
96+
/// <description>
97+
/// Attributes are case-insensitive but are conventionally written in uppercase. Note that
98+
/// attribute names are more verbose in some X.500 implementations.
99+
/// </description>
100+
/// </item>
101+
/// </list>
102+
/// <list type="table">
103+
/// <listheader>
104+
/// <term>
105+
/// LDAP
106+
/// </term>
107+
/// <description>
108+
/// A streamlined version of the X.500 DN, tailored for LDAP (Lightweight Directory Access Protocol).
109+
/// </description>
110+
/// </listheader>
111+
/// <item>
112+
/// <term>
113+
/// Separator
114+
/// </term>
115+
/// <description>
116+
/// Components are separated by commas (<c>,</c>), like X.500, but semicolons (<c>;</c>) are also sometimes
117+
/// allowed (albeit less common).
118+
/// </description>
119+
/// </item>
120+
/// <item>
121+
/// <term>
122+
/// Order
123+
/// </term>
124+
/// <description>
125+
/// Attributes are typically listed in the least significant to most significant order (<i>leaf</i> to <i>root</i>),
126+
/// though many implementations allow flexibility.
127+
/// </description>
128+
/// </item>
129+
/// <item>
130+
/// <term>
131+
/// Attributes
132+
/// </term>
133+
/// <description>
134+
/// LDAP uses abbreviated attribute names (e.g., CN for common name, O for organization, OU for organizational unit).
135+
/// It is also more permissive with regards to case sensitivity and attribute formatting.
136+
/// </description>
137+
/// </item>
138+
/// </list>
139+
/// </remarks>
140+
/// <seealso href="https://datatracker.ietf.org/doc/html/rfc4514"/>
141+
public static bool TryParseDistinguishedName(string distinguishedName, out DistinguishedName? result)
50142
{
51143
var rdns = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
52144
var dn = new X500DistinguishedName(distinguishedName);
@@ -61,21 +153,30 @@ public static DistinguishedName ParseDistinguishedName(string distinguishedName)
61153
rdns.Add(attribute, value);
62154
}
63155

64-
rdns.TryGetValue(RDN.C, out string? country);
65-
rdns.TryGetValue(RDN.CN, out string? commonName);
66-
rdns.TryGetValue(RDN.L, out string? locality);
67-
rdns.TryGetValue(RDN.O, out string? organization);
68-
rdns.TryGetValue(RDN.OU, out string? organizationalUnit);
69-
rdns.TryGetValue(RDN.S, out string? state);
156+
bool hasCountry = rdns.TryGetValue(RDN.C, out string? country);
157+
bool hasCommonName = rdns.TryGetValue(RDN.CN, out string? commonName);
158+
bool hasLocality = rdns.TryGetValue(RDN.L, out string? locality);
159+
bool hasOrganization = rdns.TryGetValue(RDN.O, out string? organization);
160+
bool hasOrganizationalUnit = rdns.TryGetValue(RDN.OU, out string? organizationalUnit);
161+
bool hasState = rdns.TryGetValue(RDN.S, out string? state);
70162

71-
return new DistinguishedName
163+
bool hasRelativeDistinguishedNames = hasCountry
164+
|| hasCommonName
165+
|| hasLocality
166+
|| hasOrganization
167+
|| hasOrganizationalUnit
168+
|| hasState;
169+
170+
result = hasRelativeDistinguishedNames ? new DistinguishedName
72171
{
73172
CommonName = commonName,
74173
OrganizationalUnit = organizationalUnit,
75174
Organization = organization,
76175
Locality = locality,
77176
State = state,
78177
Country = country,
79-
};
178+
} : null;
179+
180+
return hasRelativeDistinguishedNames;
80181
}
81182
}

0 commit comments

Comments
 (0)