Entonces, parece que el principal problema es doble:
- El "nombre descriptivo" para "Nombre alternativo del sujeto" difiere según el sistema operativo.
- La cadena formateada difiere según el sistema operativo.
Ventanas:
Friendly name: "Subject Alternative Name"
Formatted data:
DNS Name=*.stackexchange.com
DNS Name=stackexchange.com
DNS Name=stackoverflow.com
DNS Name=*.stackoverflow.com
DNS Name=stackauth.com
DNS Name=sstatic.net
DNS Name=*.sstatic.net
DNS Name=serverfault.com
DNS Name=*.serverfault.com
DNS Name=superuser.com
DNS Name=*.superuser.com
DNS Name=stackapps.com
DNS Name=openid.stackauth.com
DNS Name=*.meta.stackexchange.com
DNS Name=meta.stackexchange.com
DNS Name=mathoverflow.net
DNS Name=*.mathoverflow.net
DNS Name=askubuntu.com
DNS Name=*.askubuntu.com
DNS Name=stacksnippets.net
DNS Name=*.blogoverflow.com
DNS Name=blogoverflow.com
DNS Name=*.meta.stackoverflow.com
DNS Name=*.stackoverflow.email
DNS Name=stackoverflow.email
DNS Name=stackoverflow.blog
Linux en Docker (y Mac, aparentemente):
Friendly name: "X509v3 Subject Alternative Name"
Formatted data:
DNS:*.stackexchange.com, DNS:stackexchange.com, DNS:stackoverflow.com, DNS:*.stackoverflow.com, DNS:stackauth.com, DNS:sstatic.net, DNS:*.sstatic.net, DNS:serverfault.com, DNS:*.serverfault.com, DNS:superuser.com, DNS:*.superuser.com, DNS:stackapps.com, DNS:openid.stackauth.com, DNS:*.meta.stackexchange.com, DNS:meta.stackexchange.com, DNS:mathoverflow.net, DNS:*.mathoverflow.net, DNS:askubuntu.com, DNS:*.askubuntu.com, DNS:stacksnippets.net, DNS:*.blogoverflow.com, DNS:blogoverflow.com, DNS:*.meta.stackoverflow.com, DNS:*.stackoverflow.email, DNS:stackoverflow.email, DNS:stackoverflow.blog
Para resolver el problema n. ° 1, comencé a usar el valor en lugar del nombre descriptivo:2.5.29.17
Para resolver el problema n. ° 2, tuve que actualizar ligeramente mi expresión regular. Si bien funcionó bien para datos de varias líneas (Windows), no funcionó bien para los datos separados por comas de Linux/Mac.
La nueva expresión es ahora:((DNS:|DNS Name=|CN=)(?<domain>.*?),?(?=\s|$))+
Poniendo todo junto, obtenemos:
const string ALT_NAME_VALUE = "2.5.29.17";
string certificateData = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlJUERDQ0J5U2dBd0lCQWdJUUIyWEdUblRsa2RhQU9jb3FoSFZqOERBTkJna3Foa2lHOXcwQkFRc0ZBREJ3DQpNUXN3Q1FZRFZRUUdFd0pWVXpFVk1CTUdBMVVFQ2hNTVJHbG5hVU5sY25RZ1NXNWpNUmt3RndZRFZRUUxFeEIzDQpkM2N1WkdsbmFXTmxjblF1WTI5dE1TOHdMUVlEVlFRREV5WkVhV2RwUTJWeWRDQlRTRUV5SUVocFoyZ2dRWE56DQpkWEpoYm1ObElGTmxjblpsY2lCRFFUQWVGdzB4T0RFd01EVXdNREF3TURCYUZ3MHhPVEE0TVRReE1qQXdNREJhDQpNR294Q3pBSkJnTlZCQVlUQWxWVE1Rc3dDUVlEVlFRSUV3Sk9XVEVSTUE4R0ExVUVCeE1JVG1WM0lGbHZjbXN4DQpIVEFiQmdOVkJBb1RGRk4wWVdOcklFVjRZMmhoYm1kbExDQkpibU11TVJ3d0dnWURWUVFEREJNcUxuTjBZV05yDQpaWGhqYUdGdVoyVXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE5S2s2DQpOWFVQMW9jWHQ4OW1UMWNJeGFkQmh6Q0wwWVRxUDAxL0RTb3RVSFJ6VjcwcU9DVDdBZE1UMEsxSmk2ckZ5YXB6DQpSaXFVSWhBa2hFc2VYUnAwTU5yMjFmU1V3NFZvQ2IrSW1PNmduSWx6b2xraHJwSzZJeTM0S3lVM3p5dDhYWUQrDQptWTNpOEdqUFpPeXNSSk5MeTNvdVFCbXp1T21VLzJGb21ubWlFR0YwMm1WZ2IzZXY4UHJjbnQ3ZENpRjdsaUJJDQpzZDN6a1BlWHZUVlljVmNiL01CckZFemM0RnVJdXBoVGlKYm9Oejh3SHY5K1BYQVhVVUg4VEVTclVmRlBDS0pIDQp3ZDlFQW9OWDhqUFUxVEl4aUNvZTZYTjVFMW1QeUdneXZFbmFjSC9IZXJLL2VMYzQ2TDdZV1ZHUnlqSFdhYVRLDQowckpoS2draDU5cXNXQmRuNXdJREFRQUJvNElFMWpDQ0JOSXdId1lEVlIwakJCZ3dGb0FVVVdqL2tLOENCM1U4DQp6TmxsWkdLaUVyaFpjanN3SFFZRFZSME9CQllFRkpxS3dXN0I4azM2MlhzQzFJK3pBNnhxUGNaWU1JSUIvQVlEDQpWUjBSQklJQjh6Q0NBZStDRXlvdWMzUmhZMnRsZUdOb1lXNW5aUzVqYjIyQ0VYTjBZV05yWlhoamFHRnVaMlV1DQpZMjl0Z2hGemRHRmphMjkyWlhKbWJHOTNMbU52YllJVEtpNXpkR0ZqYTI5MlpYSm1iRzkzTG1OdmJZSU5jM1JoDQpZMnRoZFhSb0xtTnZiWUlMYzNOMFlYUnBZeTV1WlhTQ0RTb3VjM04wWVhScFl5NXVaWFNDRDNObGNuWmxjbVpoDQpkV3gwTG1OdmJZSVJLaTV6WlhKMlpYSm1ZWFZzZEM1amIyMkNEWE4xY0dWeWRYTmxjaTVqYjIyQ0R5b3VjM1Z3DQpaWEoxYzJWeUxtTnZiWUlOYzNSaFkydGhjSEJ6TG1OdmJZSVViM0JsYm1sa0xuTjBZV05yWVhWMGFDNWpiMjJDDQpHQ291YldWMFlTNXpkR0ZqYTJWNFkyaGhibWRsTG1OdmJZSVdiV1YwWVM1emRHRmphMlY0WTJoaGJtZGxMbU52DQpiWUlRYldGMGFHOTJaWEptYkc5M0xtNWxkSUlTS2k1dFlYUm9iM1psY21ac2IzY3VibVYwZ2cxaGMydDFZblZ1DQpkSFV1WTI5dGdnOHFMbUZ6YTNWaWRXNTBkUzVqYjIyQ0VYTjBZV05yYzI1cGNIQmxkSE11Ym1WMGdoSXFMbUpzDQpiMmR2ZG1WeVpteHZkeTVqYjIyQ0VHSnNiMmR2ZG1WeVpteHZkeTVqYjIyQ0dDb3ViV1YwWVM1emRHRmphMjkyDQpaWEptYkc5M0xtTnZiWUlWS2k1emRHRmphMjkyWlhKbWJHOTNMbVZ0WVdsc2doTnpkR0ZqYTI5MlpYSm1iRzkzDQpMbVZ0WVdsc2doSnpkR0ZqYTI5MlpYSm1iRzkzTG1Kc2IyY3dEZ1lEVlIwUEFRSC9CQVFEQWdXZ01CMEdBMVVkDQpKUVFXTUJRR0NDc0dBUVVGQndNQkJnZ3JCZ0VGQlFjREFqQjFCZ05WSFI4RWJqQnNNRFNnTXFBd2hpNW9kSFJ3DQpPaTh2WTNKc015NWthV2RwWTJWeWRDNWpiMjB2YzJoaE1pMW9ZUzF6WlhKMlpYSXRaell1WTNKc01EU2dNcUF3DQpoaTVvZEhSd09pOHZZM0pzTkM1a2FXZHBZMlZ5ZEM1amIyMHZjMmhoTWkxb1lTMXpaWEoyWlhJdFp6WXVZM0pzDQpNRXdHQTFVZElBUkZNRU13TndZSllJWklBWWI5YkFFQk1Db3dLQVlJS3dZQkJRVUhBZ0VXSEdoMGRIQnpPaTh2DQpkM2QzTG1ScFoybGpaWEowTG1OdmJTOURVRk13Q0FZR1o0RU1BUUlDTUlHREJnZ3JCZ0VGQlFjQkFRUjNNSFV3DQpKQVlJS3dZQkJRVUhNQUdHR0doMGRIQTZMeTl2WTNOd0xtUnBaMmxqWlhKMExtTnZiVEJOQmdnckJnRUZCUWN3DQpBb1pCYUhSMGNEb3ZMMk5oWTJWeWRITXVaR2xuYVdObGNuUXVZMjl0TDBScFoybERaWEowVTBoQk1raHBaMmhCDQpjM04xY21GdVkyVlRaWEoyWlhKRFFTNWpjblF3REFZRFZSMFRBUUgvQkFJd0FEQ0NBUVlHQ2lzR0FRUUIxbmtDDQpCQUlFZ2ZjRWdmUUE4Z0IzQUtTNUNaQzBHRmdVaDdzVG9zeG5jQW84TlpnRStSdmZ1T04zelE3SURkd1FBQUFCDQpaa0lKK1NNQUFBUURBRWd3UmdJaEFQQ2FkeHY0N2NCNFFPT3ZOOXMvUjIzRWVwRWJTSTQvTXVBZGY1dktlVTc5DQpBaUVBMmdaM084bnp1VVZhblJXOWZnNm1nZnNMMDhObi9aR203M08vRjNJR1gyVUFkd0NIZGIvbldYejRqRU9aDQpYNzN6YnY5V2pVZFdOdjlLdFdEQnRPci9YcUNERHdBQUFXWkNDZm9HQUFBRUF3QklNRVlDSVFEUWpmbVZCcSsvDQp5MmdCSy9lRTl4Nmp6OWhUWjV0SWZoa1N0Uzg1Zk1BeGVnSWhBTUF1Tmt5dU80dDA2RWlFZ01XaWFsSlp1QW5rDQpRdzI5R2NlSUJHOHIxQXAzTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBQWs4NzMvKzJRc3dLZkpTUW4raVplDQpaYzh1aUdnOTlwbWQwMDhFMmNtR0VycU56UGNsSzlJTmJQand4bDl6SWdUY1hwRi9VdEJWVlMxWjdYbzgzcWZzDQpHTVBkTXpZdDNGK2hRcFFZTGhsR0YrNUpmMng2WmIxempUL2FNM0dNZEdpKzZPUFYrWUhsL3dnVWU0NU42a1J1DQpEWm5WTDE2NytXM2wybkQrbVNoTy83eHF3SGladXowR0h5QkhScDVpNDNibEgwdmc2N3NKOGd1TjgxcGFwdXBZDQoydjc4RmN2UjhqMEVMZmd5ZWh0V3BqRjN2ektTcS9yWTJzTVhUWXpTUHFNZWtzc2VITmhTTVlVNld3OWg5cHlNDQphVm53OXZhaHFmN25LSEhjQzJWUlRVZ2tRZm45eURtbUJPbzBuUThYZ2ZwZDY1L1BheFZmQm51S2ZFa1hCZnBNDQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tDQo=";
var secretData = Convert.FromBase64String(certificateData);
string[] urls = null;
var tmp = new X509Certificate2Collection();
tmp.Import(secretData);
var cert = tmp.OfType<X509Certificate2>().FirstOrDefault();
urls = cert.Extensions
.OfType<X509Extension>()
.Where(e => e.Oid.Value == ALT_NAME_VALUE)
// I removed the part appending the original subject because I realise the same
// information is available in the SAN object, so it was just duplicating data
// this also allowed me to combine Format with creating the AsnEncodedData object
.Select(e => new AsnEncodedData(e.Oid, e.RawData).Format(true))
.SelectMany(d => Regex.Matches(d, "((DNS:|DNS Name=|CN=)(?<domain>.*?),?(?=\\s|$))+").OfType<Match>().Where(m => m.Success).Select(m => m.Groups["domain"].Value))
.Distinct()
.ToArray();
Console.WriteLine("S");
foreach (var r in urls)
{
Console.WriteLine(r);
}
Console.WriteLine("E");
Y ahora la salida en Windows y Linux es la misma:
S
*.stackexchange.com
stackexchange.com
stackoverflow.com
*.stackoverflow.com
stackauth.com
sstatic.net
*.sstatic.net
serverfault.com
*.serverfault.com
superuser.com
*.superuser.com
stackapps.com
openid.stackauth.com
*.meta.stackexchange.com
meta.stackexchange.com
mathoverflow.net
*.mathoverflow.net
askubuntu.com
*.askubuntu.com
stacksnippets.net
*.blogoverflow.com
blogoverflow.com
*.meta.stackoverflow.com
*.stackoverflow.email
stackoverflow.email
stackoverflow.blog
E
Gracias a que Nkosi me indicó esta respuesta, pude usar PeNet para extraer las URL del certificado:
var node = Asn1Node.ReadNode(cert.RawData);
urls =
FlattenNodes(node)
.Where(n => n.Nodes.Count > 0 && n.Nodes[0] is Asn1ObjectIdentifier && ((Asn1ObjectIdentifier)n.Nodes[0]).FriendlyName == "subjectAltName")
.SelectMany(n => n.Nodes).OfType<Asn1OctetString>()
.SelectMany(o => Asn1Sequence.ReadNode(o.Data).Nodes).OfType<Asn1CustomNode>()
.Select(o => System.Text.Encoding.UTF8.GetString(o.Data))
.ToArray();
Donde FlattenNodes
parece
private IEnumerable<Asn1Node> FlattenNodes(Asn1Node parentNode)
{
yield return parentNode;
foreach (var childNode in parentNode.Nodes)
{
foreach (var grandChildNode in FlattenNodes(childNode))
{
yield return grandChildNode;
}
}
}
Sé que aplanar todos los nodos probablemente no sea la forma más eficiente de hacer esto, pero en mi caso, el rendimiento no es realmente importante. Es simplemente verificar un puñado de certificados en un clúster de Kubernetes como un trabajo cron diario.
Me interesaría saber si es posible sin una biblioteca de terceros, especialmente porque está en Windows.