这里的所有答案都是特定于平台或操作系统语言的,或者只能检索一个替代主题名称,所以我通过逆向工程原始数据编写了自己的解析器,它可以解析 DNS 和 IP 地址并满足我的需求:
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));
}
我还在 corefx 存储库本身中找到了这个测试:https ://github.com/dotnet/corefx/blob/master/src/System.Security.Cryptography.Encoding/tests/AsnEncodedData.cs#L38
想法是将asnData.Format
结果拆分为 ':'、'='、',' 并采用其他所有值,这是一种更容易的方法:
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];
}