filtering (cert-store)

This commit is contained in:
2025-11-04 11:17:27 +01:00
parent fbe64afd41
commit f382b7c47c
19 changed files with 288 additions and 57 deletions

View File

@@ -0,0 +1,11 @@
using CertMgr.Core.Filters;
namespace CertMgr.Certificates.Filters;
internal sealed class ByIssuerFilter : CertificateFilter
{
public ByIssuerFilter(string pattern, FilteringFlags flags)
: base(pattern, flags, cert => cert.Issuer)
{
}
}

View File

@@ -0,0 +1,11 @@
using CertMgr.Core.Filters;
namespace CertMgr.Certificates.Filters;
internal sealed class BySubjectFilter : CertificateFilter
{
public BySubjectFilter(string pattern, FilteringFlags flags)
: base(pattern, flags, cert => cert.Subject)
{
}
}

View File

@@ -1,21 +1,11 @@
using System.Diagnostics; using CertMgr.Core.Filters;
using System.Security.Cryptography.X509Certificates;
namespace CertMgr.Certificates.Filters; namespace CertMgr.Certificates.Filters;
internal sealed class ByThumbprintFilter : CertificateFilter internal sealed class ByThumbprintFilter : CertificateFilter
{ {
internal ByThumbprintFilter(string thumbprint) public ByThumbprintFilter(string pattern, FilteringFlags flags)
: base(CertificateFilterType.Thumbprint) : base(pattern, flags, cert => cert.Thumbprint)
{ {
Thumbprint = thumbprint;
}
private string Thumbprint { [DebuggerStepThrough] get; }
protected override Task<bool> DoIsMatchAsync(X509Certificate2 value, CancellationToken cancellationToken)
{
bool result = string.Equals(value.Thumbprint, Thumbprint, StringComparison.OrdinalIgnoreCase);
return Task.FromResult(result);
} }
} }

View File

@@ -5,17 +5,51 @@ using CertMgr.Core.Filters;
namespace CertMgr.Certificates.Filters; namespace CertMgr.Certificates.Filters;
public abstract class CertificateFilter : Filter<X509Certificate2> internal abstract class CertificateFilter : Filter<X509Certificate2>
{ {
internal CertificateFilter(CertificateFilterType type) protected CertificateFilter(string pattern, FilteringFlags flags, Func<X509Certificate2, string> propertyGetter)
{ {
Type = type; Pattern = pattern;
PropertyGetter = propertyGetter;
Worker = GetWorker(flags);
} }
public CertificateFilterType Type { [DebuggerStepThrough] get; } private string Pattern { [DebuggerStepThrough] get; }
public override string ToString() private Func<X509Certificate2, string> PropertyGetter { [DebuggerStepThrough] get; }
private IFilter<string> Worker { [DebuggerStepThrough] get; }
protected sealed override async Task<bool> DoIsMatchAsync(X509Certificate2 value, CancellationToken cancellationToken)
{ {
return string.Format("type = {0}", Type); string property = PropertyGetter(value);
bool result = await Worker.IsMatchAsync(property, cancellationToken).ConfigureAwait(false);
return result;
}
private IFilter<string> GetWorker(FilteringFlags flags)
{
IFilter<string> worker;
if (flags.HasFlag(FilteringFlags.Exact))
{
worker = Core.Filters.KnownFilters.String.ExactMatch(Pattern, flags.HasFlag(FilteringFlags.CaseSensitive));
}
else if (flags.HasFlag(FilteringFlags.Wildcard))
{
worker = Core.Filters.KnownFilters.String.Wildcard(Pattern, flags.HasFlag(FilteringFlags.CaseSensitive));
}
else if (flags.HasFlag(FilteringFlags.Regex))
{
worker = Core.Filters.KnownFilters.String.Regex(Pattern, flags.HasFlag(FilteringFlags.CaseSensitive));
}
else
{
// default search is 'Exact'
worker = Core.Filters.KnownFilters.String.ExactMatch(Pattern, flags.HasFlag(FilteringFlags.CaseSensitive));
}
return worker;
} }
} }

View File

@@ -2,5 +2,7 @@
public enum CertificateFilterType public enum CertificateFilterType
{ {
Thumbprint = 1 Thumbprint = 1,
Subject,
Issuer
} }

View File

@@ -8,9 +8,19 @@ public static class KnownFilters
{ {
public static class Certificate public static class Certificate
{ {
public static IFilter<X509Certificate2> ByThumbprint(string thumbprint) public static IFilter<X509Certificate2> ByThumbprint(string thumbprint, FilteringFlags flags)
{ {
return new ByThumbprintFilter(thumbprint); return new ByThumbprintFilter(thumbprint, flags);
}
public static IFilter<X509Certificate2> BySubject(string subjectName, FilteringFlags flags)
{
return new BySubjectFilter(subjectName, flags);
}
public static IFilter<X509Certificate2> ByIssuer(string issuerName, FilteringFlags flags)
{
return new ByIssuerFilter(issuerName, flags);
} }
} }
} }

View File

@@ -16,6 +16,11 @@ public abstract class Filter<T> : IFilter<T>
protected abstract Task<bool> DoIsMatchAsync(T value, CancellationToken cancellationToken); protected abstract Task<bool> DoIsMatchAsync(T value, CancellationToken cancellationToken);
public override string ToString()
{
return string.Format("type = {0}", GetType().ToString(false));
}
private sealed class EmptyFilter : Filter<T> private sealed class EmptyFilter : Filter<T>
{ {
internal EmptyFilter(bool result) internal EmptyFilter(bool result)

View File

@@ -0,0 +1,13 @@
namespace CertMgr.Core.Filters;
[Flags]
public enum FilteringFlags
{
None = 0,
CaseSensitive = 1 << 1,
Exact = 1 << 3,
Wildcard = 1 << 4,
Regex = 1 << 5,
}

View File

@@ -0,0 +1,29 @@
using System.Diagnostics;
using CertMgr.Core.Utils;
namespace CertMgr.Core.Filters.Impl;
internal sealed class ExactMatchStringFilter : Filter<string>
{
internal ExactMatchStringFilter(string pattern, bool caseSensitive)
{
Pattern = pattern;
CaseSensitive = caseSensitive;
}
private string Pattern { [DebuggerStepThrough] get; }
private bool CaseSensitive { [DebuggerStepThrough] get; }
protected override Task<bool> DoIsMatchAsync(string value, CancellationToken cancellationToken)
{
bool ismatch = string.Equals(Pattern, value, CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
return Task.FromResult(ismatch);
}
public override string ToString()
{
return string.Format("type = {0}, pattern = '{1}', case-sensitive = {2}", GetType().ToString(false), Pattern, CaseSensitive ? "yes" : "no");
}
}

View File

@@ -0,0 +1,27 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
using CertMgr.Core.Utils;
namespace CertMgr.Core.Filters.Impl;
internal sealed class RegexStringFilter : Filter<string>
{
internal RegexStringFilter(string regexPattern, bool caseSensitive)
{
Regex = new Regex(regexPattern, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
}
private Regex Regex { [DebuggerStepThrough] get; }
protected override Task<bool> DoIsMatchAsync(string value, CancellationToken cancellationToken)
{
bool ismatch = Regex.IsMatch(value);
return Task.FromResult(ismatch);
}
public override string ToString()
{
return string.Format("type = {0}, pattern = '{1}', case-sensitive = {2}", GetType().ToString(false), Regex, Regex.Options.HasFlag(RegexOptions.IgnoreCase) ? "no" : "yes");
}
}

View File

@@ -0,0 +1,21 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace CertMgr.Core.Filters.Impl;
internal sealed class WildcardStringFilter : Filter<string>
{
internal WildcardStringFilter(string pattern, bool caseSensitive)
{
string regexPattern = "^" + Regex.Escape(pattern).Replace("\\?", ".").Replace("\\*", ".*") + "$";
Regex = new Regex(regexPattern, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
}
private Regex Regex { [DebuggerStepThrough] get; }
protected override Task<bool> DoIsMatchAsync(string value, CancellationToken cancellationToken)
{
bool ismatch = Regex.IsMatch(value);
return Task.FromResult(ismatch);
}
}

View File

@@ -0,0 +1,24 @@
using CertMgr.Core.Filters.Impl;
namespace CertMgr.Core.Filters;
public static class KnownFilters
{
public static class String
{
public static IFilter<string> ExactMatch(string pattern, bool caseSensitive)
{
return new ExactMatchStringFilter(pattern, caseSensitive);
}
public static IFilter<string> Wildcard(string pattern, bool caseSensitive)
{
return new WildcardStringFilter(pattern, caseSensitive);
}
public static IFilter<string> Regex(string pattern, bool caseSensitive)
{
return new RegexStringFilter(pattern, caseSensitive);
}
}
}

View File

@@ -0,0 +1,8 @@
namespace CertMgr.Core.Filters;
public enum StringFilterType
{
ExactMatch = 1,
Wildcard,
Regex
}

View File

@@ -26,12 +26,34 @@ internal sealed class CertificateFilterConverter : ValueConverter<IFilter<X509Ce
ReadOnlySpan<char> filterTypeSpan = span.Slice(0, separatorIndex); ReadOnlySpan<char> filterTypeSpan = span.Slice(0, separatorIndex);
if (Enum.TryParse(filterTypeSpan, true, out CertificateFilterType filterType) && Enum.IsDefined(filterType)) if (Enum.TryParse(filterTypeSpan, true, out CertificateFilterType filterType) && Enum.IsDefined(filterType))
{ {
ReadOnlySpan<char> nameSpan = span.Slice(separatorIndex + 1, span.Length - separatorIndex - 1); ReadOnlySpan<char> valueSpan = span.Slice(separatorIndex + 1, span.Length - separatorIndex - 1);
string value = nameSpan.ToString(); separatorIndex = valueSpan.IndexOf(':');
string pattern;
FilteringFlags flags;
if (separatorIndex == -1)
{
pattern = valueSpan.ToString();
flags = FilteringFlags.Exact;
}
else
{
ReadOnlySpan<char> flagsSpan = valueSpan.Slice(0, separatorIndex);
flags = GetFilteringFlags(flagsSpan);
pattern = valueSpan.Slice(separatorIndex + 1).ToString();
}
switch (filterType) switch (filterType)
{ {
case CertificateFilterType.Thumbprint: case CertificateFilterType.Thumbprint:
filter = KnownFilters.Certificate.ByThumbprint(value); filter = Certificates.Filters.KnownFilters.Certificate.ByThumbprint(pattern, flags);
break;
case CertificateFilterType.Subject:
filter = Certificates.Filters.KnownFilters.Certificate.BySubject(pattern, flags);
break;
case CertificateFilterType.Issuer:
filter = Certificates.Filters.KnownFilters.Certificate.ByIssuer(pattern, flags);
break; break;
default: default:
break; break;
@@ -39,7 +61,7 @@ internal sealed class CertificateFilterConverter : ValueConverter<IFilter<X509Ce
} }
else else
{ {
CLog.Error("Failed to parse filter type from value '{0}'. (available values: {1})", rawValue, Enum.GetNames<CertificateFilterType>().ToSeparatedList()); CLog.Error("Failed to parse filter type from value '{0}'. (available values = {1})", rawValue, Enum.GetNames<CertificateFilterType>().ToSeparatedList());
} }
} }
else else
@@ -49,4 +71,21 @@ internal sealed class CertificateFilterConverter : ValueConverter<IFilter<X509Ce
return Task.FromResult(filter); return Task.FromResult(filter);
} }
private FilteringFlags GetFilteringFlags(ReadOnlySpan<char> flagsSpan)
{
FilteringFlags flags = FilteringFlags.None;
MemoryExtensions.SpanSplitEnumerator<char> enu = flagsSpan.Split(',');
while (enu.MoveNext())
{
ReadOnlySpan<char> currentFlag = flagsSpan[enu.Current].Trim();
if (Enum.TryParse(currentFlag, true, out FilteringFlags tmp) && Enum.IsDefined(tmp))
{
flags |= tmp;
}
}
return flags;
}
} }

View File

@@ -14,7 +14,7 @@ public sealed class GetCertificateInfoJob : Job<GetCertificateInfoSettings>
protected override async Task<JobResult> DoExecuteAsync(CancellationToken cancellationToken) protected override async Task<JobResult> DoExecuteAsync(CancellationToken cancellationToken)
{ {
X509Certificate2? cert = null; List<X509Certificate2> certs = new List<X509Certificate2>();
if (!string.IsNullOrEmpty(Settings.File)) if (!string.IsNullOrEmpty(Settings.File))
{ {
@@ -22,7 +22,8 @@ public sealed class GetCertificateInfoJob : Job<GetCertificateInfoSettings>
{ {
try try
{ {
cert = X509CertificateLoader.LoadPkcs12FromFile(Settings.File, Settings.Password); X509Certificate2 cert = X509CertificateLoader.LoadPkcs12FromFile(Settings.File, Settings.Password);
certs.Add(cert);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -39,8 +40,8 @@ public sealed class GetCertificateInfoJob : Job<GetCertificateInfoSettings>
if (Settings.Filter != null) if (Settings.Filter != null)
{ {
CertStoreSearcher searcher = new CertStoreSearcher(Settings.Store); CertStoreSearcher searcher = new CertStoreSearcher(Settings.Store);
IReadOnlyList<X509Certificate2> certs = await searcher.SearchAsync(Settings.Filter, cancellationToken).ConfigureAwait(false); IReadOnlyList<X509Certificate2> searchResult = await searcher.SearchAsync(Settings.Filter, cancellationToken).ConfigureAwait(false);
cert = certs.FirstOrDefault(); certs.AddRange(searchResult);
} }
else else
{ {
@@ -52,42 +53,48 @@ public sealed class GetCertificateInfoJob : Job<GetCertificateInfoSettings>
CLog.Error("Either file or cert-store must be specified when requesting info on certificate"); CLog.Error("Either file or cert-store must be specified when requesting info on certificate");
} }
if (cert != null) if (certs.Count > 0)
{ {
using (StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(256)) using (StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(512 * certs.Count))
{ {
int padSize = 24;
StringBuilder sb = lease.Builder; StringBuilder sb = lease.Builder;
sb.AppendLine();
AppendWithPadding(sb, "Subject", cert.Subject.StartsWith("CN=") ? cert.Subject.Substring(3) : cert.Subject, padSize); foreach (X509Certificate2 cert in certs)
AppendWithPadding(sb, "Issuer", cert.Issuer.StartsWith("CN=") ? cert.Issuer.Substring(3) : cert.Issuer, padSize);
AppendWithPadding(sb, "Not Before", cert.NotBefore.ToString("s"), padSize);
AppendWithPadding(sb, "Not After", cert.NotAfter.ToString("s"), padSize);
AppendWithPadding(sb, "Signature Algorithm", cert.SignatureAlgorithm.FriendlyName, padSize);
AppendWithPadding(sb, "Thumbprint", cert.Thumbprint, padSize);
if (!string.IsNullOrEmpty(cert.FriendlyName))
{ {
AppendWithPadding(sb, "FriendlyName", cert.FriendlyName, padSize); int padSize = 24;
} sb.AppendLine();
foreach (X509Extension ext in cert.Extensions) AppendWithPadding(sb, "Subject", cert.Subject.StartsWith("CN=") ? cert.Subject.Substring(3) : cert.Subject, padSize);
{ AppendWithPadding(sb, "Issuer", cert.Issuer.StartsWith("CN=") ? cert.Issuer.Substring(3) : cert.Issuer, padSize);
string[] values = ext.Format(true).TrimEnd(Environment.NewLine.ToCharArray()).Split(Environment.NewLine); AppendWithPadding(sb, "Not Before", cert.NotBefore.ToString("s"), padSize);
if ((values.Length > 1 && !string.IsNullOrEmpty(values[values.Length - 1])) || values.Length > 2) AppendWithPadding(sb, "Not After", cert.NotAfter.ToString("s"), padSize);
AppendWithPadding(sb, "Signature Algorithm", cert.SignatureAlgorithm.FriendlyName, padSize);
AppendWithPadding(sb, "Thumbprint", cert.Thumbprint, padSize);
if (!string.IsNullOrEmpty(cert.FriendlyName))
{ {
AppendWithPadding(sb, ext.Oid?.FriendlyName ?? ext.Oid?.Value, string.Empty, padSize); AppendWithPadding(sb, "FriendlyName", cert.FriendlyName, padSize);
foreach (string value in values) }
foreach (X509Extension ext in cert.Extensions)
{
string[] values = ext.Format(true).TrimEnd(Environment.NewLine.ToCharArray()).Split(Environment.NewLine);
if ((values.Length > 1 && !string.IsNullOrEmpty(values[values.Length - 1])) || values.Length > 2)
{ {
if (string.IsNullOrEmpty(value)) AppendWithPadding(sb, ext.Oid?.FriendlyName ?? ext.Oid?.Value, string.Empty, padSize);
foreach (string value in values)
{ {
continue; if (string.IsNullOrEmpty(value))
{
continue;
}
sb.AppendFormatLine(" - {0}", value);
} }
sb.AppendFormatLine(" - {0}", value); }
else
{
AppendWithPadding(sb, ext.Oid?.FriendlyName ?? ext.Oid?.Value, values[0], padSize);
} }
} }
else
{ sb.Append("----------------------------------------------------------------");
AppendWithPadding(sb, ext.Oid?.FriendlyName ?? ext.Oid?.Value, values[0], padSize);
}
} }
return CreateSuccess(sb.ToString()); return CreateSuccess(sb.ToString());

View File

@@ -14,7 +14,7 @@ internal static class Program
args = [ args = [
"--job=get-certificate-info", "--job=get-certificate-info",
"--cert-store=user/my", "--cert-store=user/my",
"--filter=thumbprint:b395149b6079553eea92d9a73ffc97463e8976ff" "--filter=thumbprint:regex:b39.+"
]; ];
// args = [ // args = [