How do you parse the Subject Alternate Names from an X509Certificate2?

前端 未结 8 1971
孤街浪徒
孤街浪徒 2020-12-18 19:40

Is there an easy way to get the Subject Alternate Names from an X509Certificate2 object?

        foreach (X509Extension ext in certificate.Extensions)
               


        
相关标签:
8条回答
  • 2020-12-18 20:17

    All of the answers here are either platform or OS language specific or are able to retrieve only one alternative subject name so I wrote my own parser by reverse engineering raw data which can parse DNS and IP Addresses and suits my needs:

    private const string SAN_OID = "2.5.29.17";
    
    private static int ReadLength(ref Span<byte> span)
    {
        var length = (int)span[0];
        span = span[1..];
        if ((length & 0x80) > 0)
        {
            var lengthBytes = length & 0x7F;
            length = 0;
            for (var i = 0; i < lengthBytes; i++)
            {
                length = length * 0x100 + span[0];
                span = span[1..];
            }
        }
        return length;
    }
    
    public static IList<string> ParseSubjectAlternativeNames(byte[] rawData)
    {
        var result = new List<string>(); // cannot yield results when using Span yet
        if (rawData.Length < 1 || rawData[0] != '0')
        {
            throw new InvalidDataException("They told me it will start with zero :(");
        }
    
        var data = rawData.AsSpan(1);
        var length = ReadLength(ref data);
        if (length != data.Length)
        {
            throw new InvalidDataException("I don't know who I am anymore");
        }
    
        while (!data.IsEmpty)
        {
            var type = data[0];
            data = data[1..];
    
            var partLength = ReadLength(ref data);
            if (type == 135) // ip
            {
                result.Add(new IPAddress(data[0..partLength]).ToString());
            } else if (type == 160) // upn
            {
                // not sure how to parse the part before \f
                var index = data.IndexOf((byte)'\f') + 1;
                var upnData = data[index..];
                var upnLength = ReadLength(ref upnData);
                result.Add(Encoding.UTF8.GetString(upnData[0..upnLength]));
            } else // all other
            {
                result.Add(Encoding.UTF8.GetString(data[0..partLength]));
            }
            data = data[partLength..];
        }
        return result;
    }
    
    public static IEnumerable<string> ParseSubjectAlternativeNames(X509Certificate2 cert)
    {
        return cert.Extensions
            .Cast<X509Extension>()
            .Where(ext => ext.Oid.Value.Equals(SAN_OID))
            .SelectMany(x => ParseSubjectAlternativeNames(x.RawData));
    }
    

    I also found this test in corefx repo itself: https://github.com/dotnet/corefx/blob/master/src/System.Security.Cryptography.Encoding/tests/AsnEncodedData.cs#L38

    The idea there is to just split the asnData.Format result on ':', '=', ',' and take every other value which is a much easier approach:

    byte[] sanExtension =
    {
        0x30, 0x31, 0x82, 0x0B, 0x65, 0x78, 0x61, 0x6D,
        0x70, 0x6C, 0x65, 0x2E, 0x6F, 0x72, 0x67, 0x82,
        0x0F, 0x73, 0x75, 0x62, 0x2E, 0x65, 0x78, 0x61,
        0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x6F, 0x72, 0x67,
        0x82, 0x11, 0x2A, 0x2E, 0x73, 0x75, 0x62, 0x2E,
        0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E,
        0x6F, 0x72, 0x67,
    };
    
    AsnEncodedData asnData = new AsnEncodedData(
        new Oid("2.5.29.17"),
        sanExtension);
    
    string s = asnData.Format(false);
    // Windows says: "DNS Name=example.org, DNS Name=sub.example.org, DNS Name=*.sub.example.org"
    // X-Plat (OpenSSL) says: "DNS:example.org, DNS:sub.example.org, DNS:*.sub.example.org".
    // This keeps the parsing generalized until we can get them to converge
    string[] parts = s.Split(new[] { ':', '=', ',' }, StringSplitOptions.RemoveEmptyEntries);
    // Parts is now { header, data, header, data, header, data }.
    string[] output = new string[parts.Length / 2];
    
    for (int i = 0; i < output.Length; i++)
    {
        output[i] = parts[2 * i + 1];
    }
    
    0 讨论(0)
  • 2020-12-18 20:18

    I have created a function to do this:

    private static List<string> ParseSujectAlternativeName(X509Certificate2 cert)
    {
                var result = new List<string>();
    
                var subjectAlternativeName = cert.Extensions.Cast<X509Extension>()
                                                    .Where(n => n.Oid.FriendlyName.EqualsCase(SubjectAlternativeName))
                                                    .Select(n => new AsnEncodedData(n.Oid, n.RawData))
                                                    .Select(n => n.Format(true))
                                                    .FirstOrDefault();
    
                if (subjectAlternativeName != null)
                {
                    var alternativeNames = subjectAlternativeName.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
    
                    foreach (var alternativeName in alternativeNames)
                    {
                        var groups = Regex.Match(alternativeName, @"^DNS Name=(.*)").Groups;
    
                        if (groups.Count > 0 && !String.IsNullOrEmpty(groups[1].Value))
                        {
                            result.Add(groups[1].Value);
                        }
                    }
                }
    
                return result;           
    }
    
    0 讨论(0)
  • 2020-12-18 20:24

    Solution to all Languages .NET
    This solution is an improvement of Minh Nguyen above solution so it can work in all Languages

    private static List<string> GetSujectAlternativeName(X509Certificate2 cert)
            {
                var result = new List<string>();
    
    
                var subjectAlternativeName = cert.Extensions.Cast<X509Extension>()
                                                    .Where(n => n.Oid.Value== "2.5.29.17") //n.Oid.FriendlyName=="Subject Alternative Name")
                                                    .Select(n => new AsnEncodedData(n.Oid, n.RawData))
                                                    .Select(n => n.Format(true))
                                                    .FirstOrDefault();
    
                if (subjectAlternativeName != null)
                {
                    var alternativeNames = subjectAlternativeName.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
    
                    foreach (var alternativeName in alternativeNames)
                    {
                        var groups = Regex.Match(alternativeName, @"^(.*)=(.*)").Groups; // @"^DNS Name=(.*)").Groups;
    
                        if (groups.Count > 0 && !String.IsNullOrEmpty(groups[2].Value))
                        {
                            result.Add(groups[2].Value);
                        }
                    }
                }
    
                return result;
            }
    
    0 讨论(0)
  • 2020-12-18 20:30

    Based on the answer from Minh, here is a self-contained static function that should return them all

        public static IEnumerable<string> ParseSujectAlternativeNames(X509Certificate2 cert)
        {
            Regex sanRex = new Regex(@"^DNS Name=(.*)", RegexOptions.Compiled | RegexOptions.CultureInvariant);
    
            var sanList = from X509Extension ext in cert.Extensions
                          where ext.Oid.FriendlyName.Equals("Subject Alternative Name", StringComparison.Ordinal)
                          let data = new AsnEncodedData(ext.Oid, ext.RawData)
                          let text = data.Format(true)
                          from line in text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                          let match = sanRex.Match(line)
                          where match.Success && match.Groups.Count > 0 && !string.IsNullOrEmpty(match.Groups[1].Value)
                          select match.Groups[1].Value;
    
            return sanList;
        }
    
    0 讨论(0)
  • 2020-12-18 20:32

    With .net core, its more relevant to need a cross-platform way to do this. @Jason Shuler solution is windows only, but with some extra work, can be platform-independent. I've adapted the code WCF uses to do this in the following snippet(MIT Licensed)

        // Adapted from https://github.com/dotnet/wcf/blob/a9984490334fdc7d7382cae3c7bc0c8783eacd16/src/System.Private.ServiceModel/src/System/IdentityModel/Claims/X509CertificateClaimSet.cs
        // We don't have a strongly typed extension to parse Subject Alt Names, so we have to do a workaround 
        // to figure out what the identifier, delimiter, and separator is by using a well-known extension
        // If https://github.com/dotnet/corefx/issues/22068 ever goes anywhere, we can remove this
        private static class X509SubjectAlternativeNameParser
        {
            private const string SAN_OID = "2.5.29.17";
    
            private static readonly string platform_identifier;
            private static readonly char platform_delimiter;
            private static readonly string platform_seperator;
    
            static X509SubjectAlternativeNameParser()
            {
                // Extracted a well-known X509Extension
                byte[] x509ExtensionBytes = new byte[] {
                    48, 36, 130, 21, 110, 111, 116, 45, 114, 101, 97, 108, 45, 115, 117, 98, 106, 101, 99,
                    116, 45, 110, 97, 109, 101, 130, 11, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109
                };
                const string subjectName1 = "not-real-subject-name";
    
                X509Extension x509Extension = new X509Extension(SAN_OID, x509ExtensionBytes, true);
                string x509ExtensionFormattedString = x509Extension.Format(false);
    
                // Each OS has a different dNSName identifier and delimiter
                // On Windows, dNSName == "DNS Name" (localizable), on Linux, dNSName == "DNS"
                // e.g.,
                // Windows: x509ExtensionFormattedString is: "DNS Name=not-real-subject-name, DNS Name=example.com"
                // Linux:   x509ExtensionFormattedString is: "DNS:not-real-subject-name, DNS:example.com"
                // Parse: <identifier><delimter><value><separator(s)>
    
                int delimiterIndex = x509ExtensionFormattedString.IndexOf(subjectName1) - 1;
                platform_delimiter = x509ExtensionFormattedString[delimiterIndex];
    
                // Make an assumption that all characters from the the start of string to the delimiter 
                // are part of the identifier
                platform_identifier = x509ExtensionFormattedString.Substring(0, delimiterIndex);
    
                int separatorFirstChar = delimiterIndex + subjectName1.Length + 1;
                int separatorLength = 1;
                for (int i = separatorFirstChar + 1; i < x509ExtensionFormattedString.Length; i++)
                {
                    // We advance until the first character of the identifier to determine what the
                    // separator is. This assumes that the identifier assumption above is correct
                    if (x509ExtensionFormattedString[i] == platform_identifier[0])
                    {
                        break;
                    }
    
                    separatorLength++;
                }
    
                platform_seperator = x509ExtensionFormattedString.Substring(separatorFirstChar, separatorLength);
            }
    
            public static IEnumerable<string> ParseSubjectAlternativeNames(X509Certificate2 cert)
            {
                return cert.Extensions
                    .Cast<X509Extension>()
                    .Where(ext => ext.Oid.Value.Equals(SAN_OID)) // Only use SAN extensions
                    .Select(ext => new AsnEncodedData(ext.Oid, ext.RawData).Format(false)) // Decode from ASN
                    // This is dumb but AsnEncodedData.Format changes based on the platform, so our static initialization code handles making sure we parse it correctly
                    .SelectMany(text => text.Split(platform_seperator, StringSplitOptions.RemoveEmptyEntries))
                    .Select(text => text.Split(platform_delimiter))
                    .Where(x => x[0] == platform_identifier)
                    .Select(x => x[1]);
            }
        }
    
    0 讨论(0)
  • 2020-12-18 20:38

    To get the "Subject Alternative Name" from a certificate:

    X509Certificate2 cert = /* your code here */;
    
    Console.WriteLine("UpnName : {0}{1}", cert.GetNameInfo(X509NameType.UpnName, false), Environment.NewLine);
    
    0 讨论(0)
提交回复
热议问题