Reputation: 13133
Is there an easy way to get the Subject Alternate Names from an X509Certificate2 object?
foreach (X509Extension ext in certificate.Extensions)
{
if (ext.Oid.Value.Equals(/* SAN OID */"2.5.29.17"))
{
byte[] raw = ext.RawData;
// ?????? parse to get type and name ????????
}
}
Upvotes: 29
Views: 30501
Reputation: 82336
Fixed all the broken code from here.
Here's how I do it:
// Function to parse and extract SAN values from the RawData
private static System.Collections.Generic.List<string> GetSubjectAlternativeNames(byte[] rawData)
{
System.Collections.Generic.List<string> sanValues = new System.Collections.Generic.List<string>();
System.Formats.Asn1.Asn1Tag dnsNameTag = new System.Formats.Asn1.Asn1Tag(System.Formats.Asn1.TagClass.ContextSpecific, tagValue: 2, isConstructed: false);
System.Formats.Asn1.Asn1Tag ipTag = new System.Formats.Asn1.Asn1Tag(System.Formats.Asn1.TagClass.ContextSpecific, tagValue: 7, isConstructed: false);
try
{
// Initialize an AsnValueReader to decode the ASN.1 DER-encoded SAN data
System.Formats.Asn1.AsnReader asnReader = new System.Formats.Asn1.AsnReader(rawData, System.Formats.Asn1.AsnEncodingRules.DER);
System.Formats.Asn1.AsnReader sequenceReader = asnReader.ReadSequence();
while (sequenceReader.HasData)
{
System.Formats.Asn1.Asn1Tag tag = sequenceReader.PeekTag();
if (tag == dnsNameTag)
{
string dnsName = sequenceReader.ReadCharacterString(System.Formats.Asn1.UniversalTagNumber.IA5String, dnsNameTag);
sanValues.Add(dnsName);
continue;
} // End if (tag == dnsNameTag)
else if (tag == ipTag)
{
byte[] ipAddressBytes = sequenceReader.ReadOctetString(ipTag);
// Convert the byte array to an IP address string (IPv4 or IPv6)
if (ipAddressBytes.Length == 4) // IPv4
{
string ipv4Address = new System.Net.IPAddress(ipAddressBytes).ToString();
sanValues.Add(ipv4Address);
}
else if (ipAddressBytes.Length == 16) // IPv6
{
System.Net.IPAddress address = new System.Net.IPAddress(ipAddressBytes);
string ipv6Address = address.IsIPv4MappedToIPv6 ? address.MapToIPv4().ToString() : address.ToString();
sanValues.Add(ipv6Address);
}
else
{
// If the length doesn't match IPv4 or IPv6, log or handle as needed
sanValues.Add("Invalid IP address format");
}
continue;
} // End else if (tag == ipTag)
sequenceReader.ReadEncodedValue();
} // Whend
}
catch (System.Formats.Asn1.AsnContentException e)
{
System.Console.WriteLine("Error parsing SAN extension: " + e.Message);
}
return sanValues;
} // End Function GetSubjectAlternativeNames
Usage:
System.Security.Cryptography.X509Certificates.X509Certificate2? certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(@"D:\mycert.pfx", "");
System.Security.Cryptography.X509Certificates.X509Extension? subjectAlternativeNames = certificate.Extensions["2.5.29.17"]; // OID for Subject Alternative Name
if (subjectAlternativeNames != null)
{
// Decode the Subject Alternative Name extension
System.Security.Cryptography.AsnEncodedData sanData = new System.Security.Cryptography.AsnEncodedData(subjectAlternativeNames.Oid, subjectAlternativeNames.RawData);
string san = sanData.Format(true);
// System.Console.WriteLine("Subject Alternative Names: " + san);
System.Console.WriteLine("subjectAlternativeNames: ");
var ls = GetSubjectAlternativeNames(subjectAlternativeNames.RawData);
foreach (string nvp in san.Split(new string[] { System.Environment.NewLine }, System.StringSplitOptions.RemoveEmptyEntries))
{
string[] parts = nvp.Split('=');
string name = parts[0];
string? value = (parts.Length > 0) ? parts[1] : null;
System.Console.WriteLine(name + ": " + value);
}
}
else
{
System.Console.WriteLine("No Subject Alternative Names found.");
}
Upvotes: 1
Reputation: 2955
For .NET 7 and above:
foreach (X509Extension ext in certificate.Extensions)
{
if (ext is X509SubjectAlternativeNameExtension san)
{
var dnsNames = san.EnumerateDnsNames();
}
}
Upvotes: 1
Reputation: 8414
Here's a solution that does not require parsing the text returned by AsnEncodedData.Format()
(but requires .NET 5 or the System.Formats.Asn1 NuGet package):
using System.Formats.Asn1;
...
public static List<string> GetAlternativeDnsNames(X509Certificate2 cert)
{
const string SAN_OID = "2.5.29.17";
var extension = cert.Extensions[SAN_OID];
if (extension is null)
{
return new List<string>();
}
// Tag value "2" is defined by:
//
// dNSName [2] IA5String,
//
// in: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6
var dnsNameTag = new Asn1Tag(TagClass.ContextSpecific, tagValue: 2, isConstructed: false);
var asnReader = new AsnReader(extension.RawData, AsnEncodingRules.BER);
var sequenceReader = asnReader.ReadSequence(Asn1Tag.Sequence);
var resultList = new List<string>();
while (sequenceReader.HasData)
{
var tag = sequenceReader.PeekTag();
if (tag != dnsNameTag)
{
sequenceReader.ReadEncodedValue();
continue;
}
var dnsName = sequenceReader.ReadCharacterString(UniversalTagNumber.IA5String, dnsNameTag);
resultList.Add(dnsName);
}
return resultList;
}
Upvotes: 12
Reputation: 360
OS independent approach
This code snippet assumes there are two possible delimiters '=' and ':'
private static List<string> GetSubjectAlternativeNames(X509Certificate2 cert)
{
var result = new List<string>();
var subjectAlternativeNames = cert.Extensions.Cast<X509Extension>()
.Where(n => n.Oid.Value == "2.5.29.17") //Subject Alternative Name
.Select(n => new AsnEncodedData(n.Oid, n.RawData))
.Select(n => n.Format(true))
.FirstOrDefault();
// Example outputs:
// Windows: "DNS Name=example.com, DNS Name=www.example.com"
// Windows NL: "DNS-naam=example.com\r\nDNS-naam=www.example.com\r\n"
// Linux: "DNS:example.com, DNS:www.example.com"
var delimiters = new char[] { '=', ':' };
var notfound = -1;
var pairs = subjectAlternativeNames.Split(new[] { ",", "\r\n", "\r", "\n" }, StringSplitOptions.TrimEntries);
foreach (var pair in pairs)
{
int position = pair.IndexOfAny(delimiters);
if (position == notfound)
continue;
var subjectAlternativeName = pair.Substring(position + 1);
if (String.IsNullOrEmpty(subjectAlternativeName))
continue;
result.Add(subjectAlternativeName);
}
return result;
}
Upvotes: 1
Reputation: 69
Expanding on Minh Nguyen's Answer taking into account using OID i rewrote it as a extension
namespace MyExtensions
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
public static class X509Certificate2Extensions
{
private const string SubjectAlternateNameOID = "2.5.29.17";
private static readonly Regex _dnsNameRegex = new Regex(@"^DNS Name=(.+)");
public static List<string> SubjectAlternativeNames(this X509Certificate2 cert)
{
var subjectAlternativeName = cert.Extensions.Cast<X509Extension>()
.Where(n => n.Oid.Value == SubjectAlternateNameOID)
.Select(n => new AsnEncodedData(n.Oid, n.RawData))
.Select(n => n.Format(true))
.FirstOrDefault();
return string.IsNullOrWhiteSpace(subjectAlternativeName)
? new List<string>()
: subjectAlternativeName.Split(new[] {"\r\n", "\r", "\n"}, StringSplitOptions.RemoveEmptyEntries)
.Select(n => _dnsNameRegex.Match(n))
.Where(r => r.Success && !string.IsNullOrWhiteSpace(r.Groups[1].Value))
.Select(r => r.Groups[1].Value)
.ToList();
}
}
}
Upvotes: 3
Reputation: 5343
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];
}
Upvotes: 2
Reputation: 483
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;
}
Upvotes: 5
Reputation: 883
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]);
}
}
Upvotes: 3
Reputation: 399
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;
}
Upvotes: 4
Reputation: 2201
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;
}
Upvotes: 1
Reputation: 85665
Use the Format method of the extension for a printable version.
X509Certificate2 cert = /* your code here */;
foreach (X509Extension extension in cert.Extensions)
{
// Create an AsnEncodedData object using the extensions information.
AsnEncodedData asndata = new AsnEncodedData(extension.Oid, extension.RawData);
Console.WriteLine("Extension type: {0}", extension.Oid.FriendlyName);
Console.WriteLine("Oid value: {0}",asndata.Oid.Value);
Console.WriteLine("Raw data length: {0} {1}", asndata.RawData.Length, Environment.NewLine);
Console.WriteLine(asndata.Format(true));
}
Upvotes: 28
Reputation: 237
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);
Upvotes: 22