'Extract Common Name from Distinguished Name
Is there a call in .NET that parses the CN from a rfc-2253 encoded distinguished name? I know there are some third-party libraries that do this, but I would prefer to use native .NET libraries if possible.
Examples of a string encoded DN
CN=L. Eagle,O=Sue\, Grabbit and Runn,C=GB
CN=Jeff Smith,OU=Sales,DC=Fabrikam,DC=COM
Solution 1:[1]
If you are working with an X509Certificate2, there is a native method that you can use to extract the Simple Name. The Simple Name is equivalent to the Common Name RDN within the Subject field of the main certificate:
x5092Cert.GetNameInfo(X509NameType.SimpleName, false);
Alternatively, X509NameType.DnsName can be used to retrieve the Subject Alternative Name, if present; otherwise, it will default to the Common Name:
x5092Cert.GetNameInfo(X509NameType.DnsName, false);
Solution 2:[2]
After digging around in the .NET source code it looks like there is an internal utility class that can parse Distinguished Names into their different components. Unfortunately the utility class is not made public, but you can access it using reflection:
string dn = "CN=TestGroup,OU=Groups,OU=UT-SLC,OU=US,DC=Company,DC=com";
Assembly dirsvc = Assembly.Load("System.DirectoryServices");
Type asmType = dirsvc.GetType("System.DirectoryServices.ActiveDirectory.Utils");
MethodInfo mi = asmType.GetMethod("GetDNComponents", BindingFlags.NonPublic | BindingFlags.Static);
string[] parameters = { dn };
var test = mi.Invoke(null, parameters);
//test.Dump("test1");//shows details when using Linqpad
//Convert Distinguished Name (DN) to Relative Distinguished Names (RDN)
MethodInfo mi2 = asmType.GetMethod("GetRdnFromDN", BindingFlags.NonPublic | BindingFlags.Static);
var test2 = mi2.Invoke(null, parameters);
//test2.Dump("test2");//shows details when using Linqpad
The results would look like this:
//test1 is array of internal "Component" struct that has name/values as strings
Name Value
CN TestGroup
OU Groups
OU UT-SLC
OU US
DC company
DC com
//test2 is a string with CN=RDN
CN=TestGroup
Please not this is an internal utility class and could change in a future release.
Solution 3:[3]
I had the same question, myself, when I found yours. Didn't find anything in the BCL; however, I did stumble across this CodeProject article that hit the nail squarely on the head.
I hope it helps you out, too.
http://www.codeproject.com/Articles/9788/An-RFC-2253-Compliant-Distinguished-Name-Parser
Solution 4:[4]
Do Win32 functions count? You can use PInvoke with DsGetRdnW. For code, see my answer to another question: https://stackoverflow.com/a/11091804/628981.
Solution 5:[5]
You can extract the common name from an ASN.1-encoded distinguished name using AsnEncodedData class:
var distinguishedName= new X500DistinguishedName("CN=TestGroup,OU=Groups,OU=UT-SLC,OU=US,DC=Company,DC=com");
var commonNameData = new AsnEncodedData("CN", distinguishedName.RawData);
var commonName = commonNameData.Format(false);
A downside of this approach is that if you specify an unrecognized OID or the field identified with the OID is missing in the distinguished name, Format method will return a hex string with the encoded value of full distinguished name so you may want to verify the result.
Also the documentation does not seem to specify if the rawData parameter of the AsnEncodedData constructor is allowed to contain other OIDs besides the one specified as the first argument so it may break on non-Windows OS or in a future version of .NET Framework.
Solution 6:[6]
Could you not just retrieve the CN attribute values?
As you correctly note, use someone else's class as there are lots of fun edge cases (escaped commas, escaped other characters) that make parsing a DN look easy, but actually reasonably tricky.
I usually use a Java class that comes with the Novell (Now NetID) Identity Manager. So that is not helpful.
Solution 7:[7]
How about this one:
string cnPattern = @"^CN=(?<cn>.+?)(?<!\\),";
string dn = @"CN=Doe\, John,OU=My OU,DC=domain,DC=com";
Regex re = new Regex(cnPattern);
Match m = re.Match(dn);
if (m.Success)
{
// Item with index 1 returns the first group match.
string cn = m.Groups[1].Value;
}
Adapted from Powershell Regular Expression for Extracting Parts of an Active Directory Distiniguished Name.
Solution 8:[8]
Just adding my two cents here. This implementation works "best" if you first learn what business rules are in place that will ultimately dictate how much of the RFC will ever be implemented at your company.
private static string ExtractCN(string distinguishedName)
{
// CN=...,OU=...,OU=...,DC=...,DC=...
string[] parts;
parts = distinguishedName.Split(new[] { ",DC=" }, StringSplitOptions.None);
var dc = parts.Skip(1);
parts = parts[0].Split(new[] { ",OU=" }, StringSplitOptions.None);
var ou = parts.Skip(1);
parts = parts[0].Split(new[] { ",CN=" }, StringSplitOptions.None);
var cnMulti = parts.Skip(1);
var cn = parts[0];
if (!Regex.IsMatch(cn, "^CN="))
throw new CustomException(string.Format("Unable to parse distinguishedName for commonName ({0})", distinguishedName));
return Regex.Replace(cn, "^CN=", string.Empty);
}
Solution 9:[9]
You could use regular expressions to do this. Here's a regex pattern than can parse the whole DN, then you can just take the parts you are interested in:
(?:^|,\s?)(?:(?<name>[A-Z]+)=(?<val>"(?:[^"]|"")+"|(?:\\,|[^,])+))+
Here it is formatted a bit nicer, and with some comments:
(?:^|,\s?) <-- Start or a comma
(?:
(?<name>[A-Z]+)
=
(?<val>
"(?:[^"]|"")+" <-- Quoted strings
|
(?:\\,|[^,])+ <-- Unquoted strings
)
)+
This regex will give you name and val capture groups for each match.
DN strings can optionally be quoted (e.g. "Hello", which allows them to contain unescaped commas. Alternatively, if not quoted, commas must be escaped with a backslash (e.g. Hello\, there!). This regex handles both quoted and unquoted strings.
Here's a link so you can see it in action: https://regex101.com/r/7vhdDz/1
Solution 10:[10]
If the order is uncertain, I do this:
private static string ExtractCN(string dn)
{
string[] parts = dn.Split(new char[] { ',' });
for (int i = 0; i < parts.Length; i++)
{
var p = parts[i];
var elems = p.Split(new char[] { '=' });
var t = elems[0].Trim().ToUpper();
var v = elems[1].Trim();
if (t == "CN")
{
return v;
}
}
return null;
}
Solution 11:[11]
This is my almost RFC-compliant fail-safe DN parser derived from https://www.codeproject.com/Articles/9788/An-RFC-2253-Compliant-Distinguished-Name-Parser and an example of its usage (extract subject name as CN and O, both optional, concatenated with comma):
private static string GetCertificateString(X509Certificate2 certificate)
{
var subjectComponents = certificate.Subject.ParseDistinguishedName();
var subjectName = string.Join(", ", subjectComponents
.Where(m => (m.Item1 == "CN") || (m.Item1 == "O"))
.Select(n => n.Item2)
.Distinct());
return $"{certificate.SerialNumber} {certificate.NotBefore:yyyy.MM.dd}-{certificate.NotAfter:yyyy.MM.dd} {subjectName}";
}
private enum DistinguishedNameParserState
{
Component,
QuotedString,
EscapedCharacter,
};
public static IEnumerable<Tuple<string, string>> ParseDistinguishedName(this string value)
{
var previousState = DistinguishedNameParserState.Component;
var currentState = DistinguishedNameParserState.Component;
var currentComponent = new StringBuilder();
var previousChar = char.MinValue;
var position = 0;
Func<StringBuilder, Tuple<string, string>> parseComponent = sb =>
{
var s = sb.ToString();
sb.Clear();
var index = s.IndexOf('=');
if (index == -1)
{
return null;
}
var item1 = s.Substring(0, index).Trim().ToUpper();
var item2 = s.Substring(index + 1).Trim();
return Tuple.Create(item1, item2);
};
while (position < value.Length)
{
var currentChar = value[position];
switch (currentState)
{
case DistinguishedNameParserState.Component:
switch (currentChar)
{
case ',':
case ';':
// Separator found, yield parsed component
var component = parseComponent(currentComponent);
if (component != null)
{
yield return component;
}
break;
case '\\':
// Escape character found
previousState = currentState;
currentState = DistinguishedNameParserState.EscapedCharacter;
break;
case '"':
// Quotation mark found
if (previousChar == currentChar)
{
// Double quotes inside quoted string produce single quote
currentComponent.Append(currentChar);
}
currentState = DistinguishedNameParserState.QuotedString;
break;
default:
currentComponent.Append(currentChar);
break;
}
break;
case DistinguishedNameParserState.QuotedString:
switch (currentChar)
{
case '\\':
// Escape character found
previousState = currentState;
currentState = DistinguishedNameParserState.EscapedCharacter;
break;
case '"':
// Quotation mark found
currentState = DistinguishedNameParserState.Component;
break;
default:
currentComponent.Append(currentChar);
break;
}
break;
case DistinguishedNameParserState.EscapedCharacter:
currentComponent.Append(currentChar);
currentState = previousState;
currentChar = char.MinValue;
break;
}
previousChar = currentChar;
position++;
}
// Yield last parsed component, if any
if (currentComponent.Length > 0)
{
var component = parseComponent(currentComponent);
if (component != null)
{
yield return component;
}
}
}
Solution 12:[12]
Sorry for being a bit late to the party, but I was able to call the Name attribute directly from c#
UserPrincipal p
and then I was able to call
p.Name
and that gave me the full name (Common Name)
Sample code:
string name;
foreach(UserPrincipal p in PSR)
{
//PSR refers to PrincipalSearchResult
name = p.Name;
Console.WriteLine(name);
}
Obviously, you will have to fill in the blanks. But this should be easier than parsing regex.
Solution 13:[13]
Well, Here I am another person late to the party. Here is my Solution:
var dn = new X500DistinguishedName("CN=TestGroup,OU=Groups,OU=UT-SLC,OU=US,DC=\"Company, inc\",DC=com");
foreach(var part in dn.Format(true).Split("\r\n"))
{
if(part == "") continue;
var parts = part.Split('=', 2);
var key = parts[0];
var value = parts[1];
// use your key and value as you see fit here.
}
Basically its leveraging the X500DistinguishedName.Format method to put things on lines. Then split by lines, then split each line into key value.
Solution 14:[14]
If you are on Windows, @MaxKiselev's answer works perfectly. On non-Windows platforms, it returns the ASN1 dumps of each attribute.
.Net Core 5+ includes an ASN1 parser, so you can access the RDN's in a cross-platform manner by using AsnReader.
Helper class:
public static class X509DistinguishedNameExtensions
{
public static IEnumerable<KeyValuePair<string, string>> GetRelativeNames(this X500DistinguishedName dn)
{
var reader = new AsnReader(dn.RawData, AsnEncodingRules.BER);
var snSeq = reader.ReadSequence();
if (!snSeq.HasData)
{
throw new InvalidOperationException();
}
// Many types are allowable. We're only going to support the string-like ones
// (This excludes IPAddress, X400 address, and other wierd stuff)
// https://www.rfc-editor.org/rfc/rfc5280#page-37
// https://www.rfc-editor.org/rfc/rfc5280#page-112
var allowedRdnTags = new[]
{
UniversalTagNumber.TeletexString, UniversalTagNumber.PrintableString,
UniversalTagNumber.UniversalString, UniversalTagNumber.UTF8String,
UniversalTagNumber.BMPString, UniversalTagNumber.IA5String,
UniversalTagNumber.NumericString, UniversalTagNumber.VisibleString,
UniversalTagNumber.T61String
};
while (snSeq.HasData)
{
var rdnSeq = snSeq.ReadSetOf().ReadSequence();
var attrOid = rdnSeq.ReadObjectIdentifier();
var attrValueTagNo = (UniversalTagNumber)rdnSeq.PeekTag().TagValue;
if (!allowedRdnTags.Contains(attrValueTagNo))
{
throw new NotSupportedException($"Unknown tag type {attrValueTagNo} for attr {attrOid}");
}
var attrValue = rdnSeq.ReadCharacterString(attrValueTagNo);
var friendlyName = new Oid(attrOid).FriendlyName;
yield return new KeyValuePair<string, string>(friendlyName ?? attrOid, attrValue);
}
}
}
Example usage:
// Subject: CN=Example, O=Organization
var cert = new X509Certificate2("foo.cer");
var names = this.cert.SubjectName.GetRelativeNames().ToArray();
// names has [ { "CN": "Example" }, { "O": "Organization" } ]
Since this does not involve any string parsing, no escape or injections can be mishandled. It doesn't support decoding DN's that contain non-string elements, but those seem exceedingly rare.
Solution 15:[15]
using System.Linq;
var dn = "CN=Jeff Smith,OU=Sales,DC=Fabrikam,DC=COM";
var cn = dn.Split(',').Where(i => i.Contains("CN=")).Select(i => i.Replace("CN=", "")).FirstOrDefault();
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
