从专有名称中提取通用名称

20

在.NET中,是否有一个API可以解析rfc-2253编码的可区分名称(Distinguished Name)中的CN信息?我知道有一些第三方库可以做到这一点,但如果可能的话,我更愿意使用原生的.NET库。

下面是一些字符串编码的DN示例:

CN=L. Eagle,O=Sue\, Grabbit and Runn,C=GB

CN=Jeff Smith,OU=Sales,DC=Fabrikam,DC=COM


你可能想要将LDAP添加到你问题的标签列表中。此外,搜索SO,找一下[LDAP C#]相关的问题。 - Jim Mischel
请查看 https://pscx.codeplex.com/SourceControl/latest#Trunk/Src/Pscx.UnitTests/DirectoryServices/DirectoryServicesTest.cs 获取在 Powershell 社区扩展中使用的代码。 - Greg Bray
未找到https://pscx.codeplex.com/SourceControl/latest#Trunk/Src/Pscx.UnitTests/DirectoryServices/DirectoryServicesTest.cs - Kiquenet
15个回答

20
如果您正在使用 X509Certificate2,则有一种方法可用于提取简单名称。简单名称等同于主证书的主题字段中的公共名称 RDN:
x5092Cert.GetNameInfo(X509NameType.SimpleName, false);

或者,如果存在的话,可以使用X509NameType.DnsName来检索主题备用名称;否则,它将默认为通用名称:

x5092Cert.GetNameInfo(X509NameType.DnsName, false);

1
如果证书具有主题备用名称,DnsName 将返回其值。例如,备用名称为 DNS Name=prog.org,此方法将返回 prog.org - Pylyp Lebediev
1
@PylypLebediev 这是正确的。更可靠的是,可以使用X509NameType.SimpleName来表示CN。 - digital_jedi

9
在浏览了.NET源代码后,发现存在一个内部工具类可以将Distinguished Names解析为它们的不同组件。不幸的是,这个工具类并没有被公开,但你可以使用反射来访问它:
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 

结果将会是这样的:
//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 

请注意,这是一个内部实用类,并且可能会在未来的发布版本中进行更改。

工作得很好,直到我需要解析多值RDN。例如cn=Robert Smith+uid=rsmith,ou=people,dc=example,dc=com。这会导致“指定的可分辨名称格式无效”的错误。 - Nathanial Woolls
GetDNComponents 的代码在这里:https://github.com/dotnet/corefx/blob/c539d6c627b169d45f0b4cf1826b560cd0862abe/src/System.DirectoryServices/src/System/DirectoryServices/ActiveDirectory/Utils.cs#L440-L449 - Cameron Taggart
单元测试中的转储 (MSTest)? - Kiquenet
类型 System.DirectoryServices.ActiveDirectory.Component[] 是内部的。Console.WriteLine("Tipo: " + test.GetType().FullName); - Kiquenet

7

4
我刚刚在这里发布了一个 NuGet 包:https://www.nuget.org/packages/DNParser/ (仅为明确起见:我不是作者,但我喜欢 NuGet 包)。 - picrap

5
如果您使用的是Windows系统,@MaxKiselev的答案非常适用。 在非Windows平台上,它会返回每个属性的ASN1转储。
.Net Core 5+包括一个ASN1解析器,因此您可以使用AsnReader 以跨平台方式访问RDN。
辅助类:
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);
        }
    }
}

示例用法:

// Subject: CN=Example, O=Organization
var cert = new X509Certificate2("foo.cer");
var names = this.cert.SubjectName.GetRelativeNames().ToArray();
// names has [ { "CN": "Example" }, { "O": "Organization" } ]

由于这不涉及任何字符串解析,因此不会处理任何转义或注入。它不支持解码包含非字符串元素的DN,但这些情况似乎极为罕见。


在Windows和Ubuntu 22.04中,使用.NET 6可以成功运行。使用本地库解析DN以获取CN或其他组件似乎是最佳方法。 - undefined

5
您可以使用AsnEncodedData类从ASN.1编码的专有名称中提取通用名称:
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);

这种方法的缺点是,如果指定了一个未被识别的OID或者与OID标识的字段在区分名称中不存在,则“Format”方法将返回一个十六进制字符串,其中包含完整区分名称的编码值,因此您可能需要验证结果。
此外,文档似乎没有明确说明AsnEncodedData构造函数的rawData参数是否允许包含除第一个参数指定的OID之外的其他OID,因此它可能会在非Windows操作系统上或将来的.NET Framework版本中出现问题。

这在非Windows平台上确实存在问题。请参见https://dev59.com/zGsz5IYBdhLWcg3wv6ju#71376270,其中提供了一个使用新的ASN1解析器替换`AsnEncodedData`功能的版本。 - Mitch

5

2

在这里,我想要添加一点意见。如果你先了解业务规则,并确定最终将在公司实施RFC的“数量”,那么此实现方式将会是“最佳”的。

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);
}

示例 CN、CN、OUCN=Usuarios del dominio,CN=Users,DC=company,DC=esCN=PRE_VPN_Usuarios,CN=Users,OU=PREPRODUCCION,DC=company,DC=esCN=PRE_Usuarios COMUNICADOS MAD,CN=Users,OU=PREPRODUCCION,DC=company,DC=esCN=PRE_Campaña TELEFONICA Agentes MAD,OU=TELEFONICA,OU=CAMPAÑAS,OU=PREPRODUCCION,DC=company,DC=es - Kiquenet
根据我的经验,“在你公司中有多少RFC会被实施”从来都不是一个绝对的规则。在某个时候,你的代码会因为有人使用了一个有效的CN(国家代码),而你的代码无法处理而失败。无论如何,我们都无法绕过需要处理整个RFC的需求。 - undefined

1
这个怎么样?
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;
}

源自Powershell正则表达式提取活动目录可区分名称的部分


2
这个正则表达式存在问题:CN不能保证出现在DN的开头,您假设在CN之后会有名称部分,并且它无法处理带引号的字符串。例如:DN=“O=Acme, OU=Acme, CN="My super, ""Great"" CN"”。 - Cocowalla
为什么CN不能在开头?谁告诉你CN可以在中间?在你的例子中,逗号必须被转义,就像引号字符一样。在DN中不使用引号来隔离DN部分的值。 - Alek Davis
请查看RFC2253的第4节 - 其中规定实现必须允许值被引号包围,并且在这些引号内,逗号不需要转义。 - Cocowalla
忘记回答关于CN位置的问题 - 我根据我在现实生活中看到的经验。您还可以在此文档和此页面中看到这方面的示例。 - Cocowalla
样例 CN、CN、OU: CN=Usuarios del dominio,CN=Users,DC=company,DC=esCN=PRE_VPN_Usuarios,CN=Users,OU=PREPRODUCCION,DC=company,DC=esCN=PRE_Usuarios COMUNICADOS MAD,CN=Users,OU=PREPRODUCCION,DC=company,DC=esCN=PRE_Campaña TELEFONICA Agentes MAD,OU=TELEFONICA,OU=CAMPAÑAS,OU=PREPRODUCCION,DC=company,DC=es - Kiquenet

1
您可以使用正则表达式来完成此操作。以下是一个可以解析整个DN的正则表达式模式,然后您只需要提取您感兴趣的部分即可:
(?:^|,\s?)(?:(?[A-Z]+)=(?"(?:[^"]|"")+"|(?:\\,|[^,])+))+
这里格式化得更好一些,并带有一些注释:
(?:^|,\s?)               <-- Start or a comma
(?:
    (?<name>[A-Z]+)
    =
    (?<val>
        "(?:[^"]|"")+"   <-- Quoted strings
        |
        (?:\\,|[^,])+    <-- Unquoted strings
    )
)+

这个正则表达式将为每个匹配提供nameval捕获组。
DN字符串可以选择加引号(例如"Hello"),这允许它们包含未转义的逗号。或者,如果没有引号,逗号必须用反斜杠转义(例如Hello\, there!)。此正则表达式处理带引号和不带引号的字符串。
这里有一个链接,让你看到它的实际效果:https://regex101.com/r/7vhdDz/1

1
我发现这个非常好,可以检查任何给定的DN格式;它运行得非常好...唯一的缺点是你需要构建一个验证方法,以确保所有的“xx=zz”属性都在支持的规则范围内,但除此之外,我真的很喜欢这个...最好的是我可以在Python中使用它。谢谢! - Larry
CN=公司用户,CN=Users,DC=company,DC=es CN=PRE_VPN_用户,CN=Users,OU=PREPRODUCCION,DC=company,DC=es CN=PRE_通讯用户 MAD,CN=Users,OU=PREPRODUCCION,DC=company,DC=es CN=PRE_电话营销代理 MAD,OU=TELEFONICA,OU=CAMPAÑAS,OU=PREPRODUCCION,DC=company,DC=es - Kiquenet
@Kiquenet,使用这些示例,正则表达式似乎可以正常工作? - Cocowalla

1
如果顺序不确定,我会这样做:
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;
}

2
包含逗号的常见名称会导致错误。例如:CN=Martin Luther King\, Jr. 会不正确地返回 Martin Luther King - Eric Eskildsen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接