Require reuse candidates to carry every requested subject alternative name
Extending -DnsName or -IpAddress and re-running returned the existing certificate, which lacked the name that had just been added - the reuse check compared only the common name. A candidate must now carry every requested name, and the name that disqualified it is reported so the reissue is explainable. Reading SANs back off an installed certificate needs a decoder: netstandard2.0 has no X509SubjectAlternativeNameExtension, and X509Extension.Format produces localized text that cannot be compared. The extension is decoded from its DER bytes with BouncyCastle, already carried for CSR generation. The rule is coverage rather than equality, since a certificate carrying more names than requested still satisfies the request. DNS names compare case-insensitively and IP addresses are normalized through IPAddress, so ::1 and 0:0:0:0:0:0:0:1 are the same name. Trimming the SAN set therefore still reuses; -Force covers that case. FindMatch keeps its original four-argument overload so existing callers are unaffected, and only reports a rejected name when no candidate qualified. Verified against a live CurrentUser\My store with a certificate carrying SANPROBE, SANPROBE.contoso.com and 10.20.30.40: identical and subset requests reuse, a new DNS name or IP forces reissue naming the missing entry, differing case reuses, and an empty request reuses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using PSInfisicalAPI.Pki;
|
||||
using Xunit;
|
||||
|
||||
namespace PSInfisicalAPI.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Reuse must not return a certificate that predates a newly requested SAN. These build real certificates
|
||||
/// through the module's own CSR path so the SAN reader is exercised against genuine DER, not a hand-rolled
|
||||
/// approximation of it.
|
||||
/// </summary>
|
||||
public class CertificateSanCoverageTests
|
||||
{
|
||||
private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly;
|
||||
|
||||
/// <summary>
|
||||
/// Produces a self-signed certificate carrying exactly the requested SANs, by round-tripping the module's
|
||||
/// CSR builder output into a signed certificate.
|
||||
/// </summary>
|
||||
private static X509Certificate2 CreateCertificateWithSans(string commonName, string[] dnsNames, string[] ipAddresses)
|
||||
{
|
||||
InfisicalCsrSubject subject = new InfisicalCsrSubject { CommonName = commonName };
|
||||
InfisicalCsrResult csr = InfisicalCsrBuilder.Build(subject, dnsNames, ipAddresses, new InfisicalCsrOptions());
|
||||
|
||||
Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest request;
|
||||
using (System.IO.StringReader reader = new System.IO.StringReader(csr.CsrPem))
|
||||
{
|
||||
Org.BouncyCastle.OpenSsl.PemReader pemReader = new Org.BouncyCastle.OpenSsl.PemReader(reader);
|
||||
request = (Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest)pemReader.ReadObject();
|
||||
}
|
||||
|
||||
Org.BouncyCastle.Asn1.Pkcs.CertificationRequestInfo info = request.GetCertificationRequestInfo();
|
||||
Org.BouncyCastle.Asn1.X509.X509Extensions extensions = null;
|
||||
foreach (Org.BouncyCastle.Asn1.Asn1Encodable attributeEncodable in info.Attributes)
|
||||
{
|
||||
Org.BouncyCastle.Asn1.Cms.Attribute attribute = Org.BouncyCastle.Asn1.Cms.Attribute.GetInstance(attributeEncodable);
|
||||
if (attribute.AttrType.Equals(Org.BouncyCastle.Asn1.Pkcs.PkcsObjectIdentifiers.Pkcs9AtExtensionRequest))
|
||||
{
|
||||
extensions = Org.BouncyCastle.Asn1.X509.X509Extensions.GetInstance(attribute.AttrValues[0]);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.NotNull(extensions);
|
||||
Org.BouncyCastle.Asn1.X509.X509Extension sanExtension =
|
||||
extensions.GetExtension(Org.BouncyCastle.Asn1.X509.X509Extensions.SubjectAlternativeName);
|
||||
Assert.NotNull(sanExtension);
|
||||
|
||||
using (RSA rsa = RSA.Create(2048))
|
||||
{
|
||||
CertificateRequest netRequest = new CertificateRequest(
|
||||
string.Concat("CN=", commonName), rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
||||
netRequest.CertificateExtensions.Add(new X509Extension(
|
||||
new Oid("2.5.29.17"),
|
||||
sanExtension.Value.GetOctets(),
|
||||
false));
|
||||
|
||||
return netRequest.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(30));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CoversRequestedNames(X509Certificate2 cert, string[] dns, string[] ips, out string missing)
|
||||
{
|
||||
Type reader = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateSanReader", true);
|
||||
MethodInfo method = reader.GetMethod("CoversRequestedNames", BindingFlags.Public | BindingFlags.Static);
|
||||
Assert.NotNull(method);
|
||||
|
||||
object[] args = new object[] { cert, dns, ips, null };
|
||||
bool result = (bool)method.Invoke(null, args);
|
||||
missing = (string)args[3];
|
||||
return result;
|
||||
}
|
||||
|
||||
private static (HashSet<string> Dns, HashSet<string> Ips) ReadSans(X509Certificate2 cert)
|
||||
{
|
||||
Type reader = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateSanReader", true);
|
||||
MethodInfo read = reader.GetMethod("Read", BindingFlags.Public | BindingFlags.Static);
|
||||
object sans = read.Invoke(null, new object[] { cert });
|
||||
|
||||
HashSet<string> dns = (HashSet<string>)sans.GetType().GetProperty("DnsNames").GetValue(sans);
|
||||
HashSet<string> ips = (HashSet<string>)sans.GetType().GetProperty("IpAddresses").GetValue(sans);
|
||||
return (dns, ips);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reader_Recovers_Both_Dns_And_Ip_Sans()
|
||||
{
|
||||
using (X509Certificate2 cert = CreateCertificateWithSans(
|
||||
"WEB01",
|
||||
new[] { "WEB01", "WEB01.contoso.com" },
|
||||
new[] { "10.20.30.40", "127.0.0.1", "::1" }))
|
||||
{
|
||||
(HashSet<string> dns, HashSet<string> ips) = ReadSans(cert);
|
||||
|
||||
Assert.Equal(2, dns.Count);
|
||||
Assert.Contains("WEB01", dns);
|
||||
Assert.Contains("WEB01.contoso.com", dns);
|
||||
|
||||
Assert.Equal(3, ips.Count);
|
||||
Assert.Contains("10.20.30.40", ips);
|
||||
Assert.Contains("127.0.0.1", ips);
|
||||
Assert.Contains("::1", ips);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_Certificate_Covering_Every_Requested_Name_Is_Reusable()
|
||||
{
|
||||
using (X509Certificate2 cert = CreateCertificateWithSans(
|
||||
"WEB01", new[] { "WEB01", "WEB01.contoso.com" }, new[] { "10.20.30.40" }))
|
||||
{
|
||||
string missing;
|
||||
Assert.True(CoversRequestedNames(cert, new[] { "WEB01", "WEB01.contoso.com" }, new[] { "10.20.30.40" }, out missing));
|
||||
Assert.Null(missing);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_Newly_Requested_Dns_Name_Disqualifies_The_Existing_Certificate()
|
||||
{
|
||||
// The reported gap: adding a name to -DnsName previously returned the old certificate.
|
||||
using (X509Certificate2 cert = CreateCertificateWithSans(
|
||||
"WEB01", new[] { "WEB01", "WEB01.contoso.com" }, null))
|
||||
{
|
||||
string missing;
|
||||
bool covers = CoversRequestedNames(
|
||||
cert,
|
||||
new[] { "WEB01", "WEB01.contoso.com", "api.contoso.com" },
|
||||
null,
|
||||
out missing);
|
||||
|
||||
Assert.False(covers);
|
||||
Assert.Equal("DNS:api.contoso.com", missing);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_Newly_Requested_Ip_Disqualifies_The_Existing_Certificate()
|
||||
{
|
||||
using (X509Certificate2 cert = CreateCertificateWithSans("WEB01", new[] { "WEB01" }, new[] { "10.20.30.40" }))
|
||||
{
|
||||
string missing;
|
||||
Assert.False(CoversRequestedNames(cert, new[] { "WEB01" }, new[] { "10.20.30.41" }, out missing));
|
||||
Assert.Equal("IP:10.20.30.41", missing);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extra_Names_On_The_Certificate_Do_Not_Disqualify_It()
|
||||
{
|
||||
// A superset still satisfies the request; only a missing name forces reissuance.
|
||||
using (X509Certificate2 cert = CreateCertificateWithSans(
|
||||
"WEB01", new[] { "WEB01", "WEB01.contoso.com", "legacy.contoso.com" }, new[] { "10.20.30.40", "127.0.0.1" }))
|
||||
{
|
||||
string missing;
|
||||
Assert.True(CoversRequestedNames(cert, new[] { "WEB01" }, new[] { "127.0.0.1" }, out missing));
|
||||
Assert.Null(missing);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dns_Comparison_Is_Case_Insensitive()
|
||||
{
|
||||
using (X509Certificate2 cert = CreateCertificateWithSans("WEB01", new[] { "WEB01.Contoso.COM" }, null))
|
||||
{
|
||||
string missing;
|
||||
Assert.True(CoversRequestedNames(cert, new[] { "web01.contoso.com" }, null, out missing));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("::1", "0:0:0:0:0:0:0:1")]
|
||||
[InlineData("0:0:0:0:0:0:0:1", "::1")]
|
||||
[InlineData("10.20.30.40", "10.20.30.40")]
|
||||
public void Ip_Comparison_Normalizes_Textual_Variations(string inCertificate, string requested)
|
||||
{
|
||||
using (X509Certificate2 cert = CreateCertificateWithSans("WEB01", new[] { "WEB01" }, new[] { inCertificate }))
|
||||
{
|
||||
string missing;
|
||||
Assert.True(CoversRequestedNames(cert, null, new[] { requested }, out missing), string.Concat("missing: ", missing));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_Certificate_Without_Any_San_Extension_Fails_A_San_Request()
|
||||
{
|
||||
using (RSA rsa = RSA.Create(2048))
|
||||
{
|
||||
CertificateRequest request = new CertificateRequest(
|
||||
"CN=NoSans", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
using (X509Certificate2 cert = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1)))
|
||||
{
|
||||
string missing;
|
||||
Assert.False(CoversRequestedNames(cert, new[] { "NoSans" }, null, out missing));
|
||||
Assert.Equal("DNS:NoSans", missing);
|
||||
|
||||
// With nothing requested there is nothing to fail on.
|
||||
Assert.True(CoversRequestedNames(cert, null, null, out missing));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindMatch_Keeps_Its_Original_Signature_For_Callers_Without_San_Requirements()
|
||||
{
|
||||
Type lookup = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalLocalCertificateLookup", true);
|
||||
MethodInfo original = lookup.GetMethod(
|
||||
"FindMatch",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
new[] { typeof(StoreName), typeof(StoreLocation), typeof(string), typeof(IEnumerable<string>) },
|
||||
null);
|
||||
Assert.NotNull(original);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
if (string.IsNullOrEmpty(csrSubject.CommonName) && dnsNames.Count > 0) { csrSubject.CommonName = dnsNames[0]; }
|
||||
if (string.IsNullOrEmpty(csrSubject.CommonName)) { throw new InvalidOperationException("Subject CommonName could not be determined and no DnsName was provided."); }
|
||||
|
||||
X509Certificate2 existing = TryFindExisting(client, connection, ProjectId, csrSubject.CommonName, resolvedStoreLocation);
|
||||
X509Certificate2 existing = TryFindExisting(client, connection, ProjectId, csrSubject.CommonName, resolvedStoreLocation, dnsNames, ipAddresses);
|
||||
if (existing != null && !Force.IsPresent && !(AllowRenewal.IsPresent && InfisicalLocalCertificateLookup.IsRenewable(existing, RenewalThresholdDays)))
|
||||
{
|
||||
Logger.Information(Component, string.Concat("Reusing existing certificate (Thumbprint=", existing.Thumbprint, ", NotAfter=", existing.NotAfter.ToString("u"), ")."));
|
||||
@@ -313,7 +313,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
/// usages and policy, so reusing one across issuers hands back a certificate that does not satisfy the
|
||||
/// request that was actually made.
|
||||
/// </summary>
|
||||
private X509Certificate2 TryFindExisting(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string commonName, StoreLocation storeLocation)
|
||||
private X509Certificate2 TryFindExisting(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string commonName, StoreLocation storeLocation, List<string> requestedDnsNames, List<string> requestedIpAddresses)
|
||||
{
|
||||
List<string> candidateSerials = new List<string>();
|
||||
bool searchCompleted = false;
|
||||
@@ -363,7 +363,18 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
"pass -Force to issue unconditionally."));
|
||||
}
|
||||
|
||||
return InfisicalLocalCertificateLookup.FindMatch(StoreName, storeLocation, commonName, candidateSerials);
|
||||
string missingName;
|
||||
X509Certificate2 match = InfisicalLocalCertificateLookup.FindMatch(
|
||||
StoreName, storeLocation, commonName, candidateSerials, requestedDnsNames, requestedIpAddresses, out missingName);
|
||||
|
||||
if (match == null && missingName != null)
|
||||
{
|
||||
Logger.Information(Component, string.Concat(
|
||||
"An existing certificate for CN=", commonName, " does not carry the requested name ", missingName,
|
||||
"; requesting a new certificate rather than reusing one that would fail validation for it."));
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Org.BouncyCastle.Asn1;
|
||||
using Org.BouncyCastle.Asn1.X509;
|
||||
using NetX509Extension = System.Security.Cryptography.X509Certificates.X509Extension;
|
||||
|
||||
namespace PSInfisicalAPI.Pki
|
||||
{
|
||||
/// <summary>
|
||||
/// The subject alternative names carried by a certificate, split the way a request specifies them.
|
||||
/// </summary>
|
||||
internal sealed class InfisicalCertificateSans
|
||||
{
|
||||
public HashSet<string> DnsNames { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
public HashSet<string> IpAddresses { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the subject alternative name extension from an installed certificate.
|
||||
/// <para>
|
||||
/// netstandard2.0 has no X509SubjectAlternativeNameExtension, and the string form produced by
|
||||
/// X509Extension.Format is localized and therefore unusable for comparison, so the extension is decoded from
|
||||
/// its DER bytes with BouncyCastle, which the module already carries for CSR generation.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class InfisicalCertificateSanReader
|
||||
{
|
||||
private const string SubjectAlternativeNameOid = "2.5.29.17";
|
||||
|
||||
public static InfisicalCertificateSans Read(X509Certificate2 cert)
|
||||
{
|
||||
InfisicalCertificateSans result = new InfisicalCertificateSans();
|
||||
if (cert == null) { return result; }
|
||||
|
||||
foreach (NetX509Extension extension in cert.Extensions)
|
||||
{
|
||||
if (extension == null || extension.Oid == null) { continue; }
|
||||
if (!string.Equals(extension.Oid.Value, SubjectAlternativeNameOid, StringComparison.Ordinal)) { continue; }
|
||||
|
||||
try
|
||||
{
|
||||
// X509Extension.RawData is the content of the extnValue OCTET STRING, so it decodes straight
|
||||
// into the GeneralNames SEQUENCE.
|
||||
Asn1Object decoded = Asn1Object.FromByteArray(extension.RawData);
|
||||
GeneralNames names = GeneralNames.GetInstance(decoded);
|
||||
if (names == null) { continue; }
|
||||
|
||||
foreach (GeneralName name in names.GetNames())
|
||||
{
|
||||
if (name == null) { continue; }
|
||||
AddName(result, name);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// A certificate this malformed cannot be matched against a request; treat it as carrying no
|
||||
// usable SANs rather than failing the caller's issuance.
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AddName(InfisicalCertificateSans target, GeneralName name)
|
||||
{
|
||||
switch (name.TagNo)
|
||||
{
|
||||
case GeneralName.DnsName:
|
||||
{
|
||||
string value = name.Name != null ? name.Name.ToString() : null;
|
||||
if (!string.IsNullOrEmpty(value)) { target.DnsNames.Add(value.Trim()); }
|
||||
break;
|
||||
}
|
||||
case GeneralName.IPAddress:
|
||||
{
|
||||
string value = FormatIpAddress(name);
|
||||
if (!string.IsNullOrEmpty(value)) { target.IpAddresses.Add(value); }
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An iPAddress general name holds raw address octets, four for IPv4 and sixteen for IPv6.
|
||||
/// </summary>
|
||||
private static string FormatIpAddress(GeneralName name)
|
||||
{
|
||||
try
|
||||
{
|
||||
Asn1OctetString octets = Asn1OctetString.GetInstance(name.Name);
|
||||
if (octets == null) { return null; }
|
||||
|
||||
byte[] bytes = octets.GetOctets();
|
||||
if (bytes == null) { return null; }
|
||||
if (bytes.Length != 4 && bytes.Length != 16) { return null; }
|
||||
|
||||
return NormalizeIpAddress(new IPAddress(bytes).ToString());
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collapses the textual variations of one address so "::1" and "0:0:0:0:0:0:0:1" compare equal.
|
||||
/// </summary>
|
||||
public static string NormalizeIpAddress(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) { return value; }
|
||||
|
||||
IPAddress parsed;
|
||||
if (IPAddress.TryParse(value.Trim(), out parsed))
|
||||
{
|
||||
return parsed.ToString();
|
||||
}
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports whether a candidate certificate carries every name the caller asked for. A certificate with
|
||||
/// extra names still satisfies the request; one missing a requested name does not, and reusing it would
|
||||
/// hand back a certificate that fails validation for the name that was added.
|
||||
/// </summary>
|
||||
public static bool CoversRequestedNames(X509Certificate2 candidate, IEnumerable<string> dnsNames, IEnumerable<string> ipAddresses, out string missingName)
|
||||
{
|
||||
missingName = null;
|
||||
if (candidate == null) { return false; }
|
||||
|
||||
InfisicalCertificateSans present = Read(candidate);
|
||||
|
||||
if (dnsNames != null)
|
||||
{
|
||||
foreach (string dns in dnsNames)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dns)) { continue; }
|
||||
if (!present.DnsNames.Contains(dns.Trim()))
|
||||
{
|
||||
missingName = string.Concat("DNS:", dns.Trim());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ipAddresses != null)
|
||||
{
|
||||
foreach (string ip in ipAddresses)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ip)) { continue; }
|
||||
if (!present.IpAddresses.Contains(NormalizeIpAddress(ip)))
|
||||
{
|
||||
missingName = string.Concat("IP:", NormalizeIpAddress(ip));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,36 @@ namespace PSInfisicalAPI.Pki
|
||||
{
|
||||
public static X509Certificate2 FindMatch(StoreName storeName, StoreLocation storeLocation, string commonName, IEnumerable<string> candidateSerialNumbers)
|
||||
{
|
||||
string ignored;
|
||||
return FindMatch(storeName, storeLocation, commonName, candidateSerialNumbers, null, null, out ignored);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the longest-lived installed certificate for a subject that also carries every requested subject
|
||||
/// alternative name. A certificate that predates a newly added SAN would fail validation for that name,
|
||||
/// so it is not a reusable answer to the current request.
|
||||
/// </summary>
|
||||
/// <param name="rejectedForMissingName">
|
||||
/// The first name that disqualified an otherwise-matching certificate, so the caller can explain why it
|
||||
/// is reissuing rather than reusing.
|
||||
/// </param>
|
||||
public static X509Certificate2 FindMatch(
|
||||
StoreName storeName,
|
||||
StoreLocation storeLocation,
|
||||
string commonName,
|
||||
IEnumerable<string> candidateSerialNumbers,
|
||||
IEnumerable<string> requiredDnsNames,
|
||||
IEnumerable<string> requiredIpAddresses,
|
||||
out string rejectedForMissingName)
|
||||
{
|
||||
rejectedForMissingName = null;
|
||||
HashSet<string> serialSet = NormalizeSerials(candidateSerialNumbers);
|
||||
string subjectFilter = !string.IsNullOrEmpty(commonName) ? string.Concat("CN=", commonName) : null;
|
||||
|
||||
List<string> dnsList = ToList(requiredDnsNames);
|
||||
List<string> ipList = ToList(requiredIpAddresses);
|
||||
bool requireSans = dnsList.Count > 0 || ipList.Count > 0;
|
||||
|
||||
X509Store store = new X509Store(storeName, storeLocation);
|
||||
try
|
||||
{
|
||||
@@ -33,12 +60,24 @@ namespace PSInfisicalAPI.Pki
|
||||
}
|
||||
}
|
||||
|
||||
if (requireSans)
|
||||
{
|
||||
string missingName;
|
||||
if (!InfisicalCertificateSanReader.CoversRequestedNames(candidate, dnsList, ipList, out missingName))
|
||||
{
|
||||
if (rejectedForMissingName == null) { rejectedForMissingName = missingName; }
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMatch == null || candidate.NotAfter > bestMatch.NotAfter)
|
||||
{
|
||||
bestMatch = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Only report a rejection when nothing else qualified; a covering certificate makes it irrelevant.
|
||||
if (bestMatch != null) { rejectedForMissingName = null; }
|
||||
return bestMatch;
|
||||
}
|
||||
finally
|
||||
@@ -47,6 +86,18 @@ namespace PSInfisicalAPI.Pki
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> ToList(IEnumerable<string> values)
|
||||
{
|
||||
List<string> result = new List<string>();
|
||||
if (values == null) { return result; }
|
||||
foreach (string value in values)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value)) { result.Add(value); }
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool IsRenewable(X509Certificate2 cert, int renewalThresholdDays)
|
||||
{
|
||||
if (cert == null) { return true; }
|
||||
|
||||
Reference in New Issue
Block a user