diff --git a/BuildOutput/bin/Debug/net9.0/certmgr.dll b/BuildOutput/bin/Debug/net9.0/certmgr.dll index 1e2cbdd..f7cebe1 100644 Binary files a/BuildOutput/bin/Debug/net9.0/certmgr.dll and b/BuildOutput/bin/Debug/net9.0/certmgr.dll differ diff --git a/BuildOutput/bin/Debug/net9.0/certmgr.exe b/BuildOutput/bin/Debug/net9.0/certmgr.exe index fcbc90a..e0f874b 100644 Binary files a/BuildOutput/bin/Debug/net9.0/certmgr.exe and b/BuildOutput/bin/Debug/net9.0/certmgr.exe differ diff --git a/BuildOutput/bin/Debug/net9.0/certmgr.pdb b/BuildOutput/bin/Debug/net9.0/certmgr.pdb index f20692e..935c900 100644 Binary files a/BuildOutput/bin/Debug/net9.0/certmgr.pdb and b/BuildOutput/bin/Debug/net9.0/certmgr.pdb differ diff --git a/certmgr/Certificates/Filters/ByIssuerFilter.cs b/certmgr/Certificates/Filters/ByIssuerFilter.cs new file mode 100644 index 0000000..1a78a21 --- /dev/null +++ b/certmgr/Certificates/Filters/ByIssuerFilter.cs @@ -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) + { + } +} diff --git a/certmgr/Certificates/Filters/BySubjectFilter.cs b/certmgr/Certificates/Filters/BySubjectFilter.cs new file mode 100644 index 0000000..2190571 --- /dev/null +++ b/certmgr/Certificates/Filters/BySubjectFilter.cs @@ -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) + { + } +} diff --git a/certmgr/Certificates/Filters/ByThumbprintFilter.cs b/certmgr/Certificates/Filters/ByThumbprintFilter.cs index 6bacfdf..0eeab9b 100644 --- a/certmgr/Certificates/Filters/ByThumbprintFilter.cs +++ b/certmgr/Certificates/Filters/ByThumbprintFilter.cs @@ -1,21 +1,11 @@ -using System.Diagnostics; -using System.Security.Cryptography.X509Certificates; +using CertMgr.Core.Filters; namespace CertMgr.Certificates.Filters; internal sealed class ByThumbprintFilter : CertificateFilter { - internal ByThumbprintFilter(string thumbprint) - : base(CertificateFilterType.Thumbprint) + public ByThumbprintFilter(string pattern, FilteringFlags flags) + : base(pattern, flags, cert => cert.Thumbprint) { - Thumbprint = thumbprint; - } - - private string Thumbprint { [DebuggerStepThrough] get; } - - protected override Task DoIsMatchAsync(X509Certificate2 value, CancellationToken cancellationToken) - { - bool result = string.Equals(value.Thumbprint, Thumbprint, StringComparison.OrdinalIgnoreCase); - return Task.FromResult(result); } } diff --git a/certmgr/Certificates/Filters/CertificateFilter.cs b/certmgr/Certificates/Filters/CertificateFilter.cs index 7cb7b9e..481dba9 100644 --- a/certmgr/Certificates/Filters/CertificateFilter.cs +++ b/certmgr/Certificates/Filters/CertificateFilter.cs @@ -5,17 +5,51 @@ using CertMgr.Core.Filters; namespace CertMgr.Certificates.Filters; -public abstract class CertificateFilter : Filter +internal abstract class CertificateFilter : Filter { - internal CertificateFilter(CertificateFilterType type) + protected CertificateFilter(string pattern, FilteringFlags flags, Func 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 PropertyGetter { [DebuggerStepThrough] get; } + + private IFilter Worker { [DebuggerStepThrough] get; } + + protected sealed override async Task 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 GetWorker(FilteringFlags flags) + { + IFilter 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; } } diff --git a/certmgr/Certificates/Filters/CertificateFilterType.cs b/certmgr/Certificates/Filters/CertificateFilterType.cs index 5eb5d9a..eefcc8e 100644 --- a/certmgr/Certificates/Filters/CertificateFilterType.cs +++ b/certmgr/Certificates/Filters/CertificateFilterType.cs @@ -2,5 +2,7 @@ public enum CertificateFilterType { - Thumbprint = 1 + Thumbprint = 1, + Subject, + Issuer } diff --git a/certmgr/Certificates/Filters/KnownFilters.cs b/certmgr/Certificates/Filters/KnownFilters.cs index 0e401fb..a7c9492 100644 --- a/certmgr/Certificates/Filters/KnownFilters.cs +++ b/certmgr/Certificates/Filters/KnownFilters.cs @@ -8,9 +8,19 @@ public static class KnownFilters { public static class Certificate { - public static IFilter ByThumbprint(string thumbprint) + public static IFilter ByThumbprint(string thumbprint, FilteringFlags flags) { - return new ByThumbprintFilter(thumbprint); + return new ByThumbprintFilter(thumbprint, flags); + } + + public static IFilter BySubject(string subjectName, FilteringFlags flags) + { + return new BySubjectFilter(subjectName, flags); + } + + public static IFilter ByIssuer(string issuerName, FilteringFlags flags) + { + return new ByIssuerFilter(issuerName, flags); } } } diff --git a/certmgr/Core/Filters/Filter.cs b/certmgr/Core/Filters/Filter.cs index f00ab82..feeb194 100644 --- a/certmgr/Core/Filters/Filter.cs +++ b/certmgr/Core/Filters/Filter.cs @@ -16,6 +16,11 @@ public abstract class Filter : IFilter protected abstract Task DoIsMatchAsync(T value, CancellationToken cancellationToken); + public override string ToString() + { + return string.Format("type = {0}", GetType().ToString(false)); + } + private sealed class EmptyFilter : Filter { internal EmptyFilter(bool result) diff --git a/certmgr/Core/Filters/FilteringFlags.cs b/certmgr/Core/Filters/FilteringFlags.cs new file mode 100644 index 0000000..15afa7a --- /dev/null +++ b/certmgr/Core/Filters/FilteringFlags.cs @@ -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, +} diff --git a/certmgr/Core/Filters/Impl/ExactMatchStringFilter.cs b/certmgr/Core/Filters/Impl/ExactMatchStringFilter.cs new file mode 100644 index 0000000..cf6ecdb --- /dev/null +++ b/certmgr/Core/Filters/Impl/ExactMatchStringFilter.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; + +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Filters.Impl; + +internal sealed class ExactMatchStringFilter : Filter +{ + internal ExactMatchStringFilter(string pattern, bool caseSensitive) + { + Pattern = pattern; + CaseSensitive = caseSensitive; + } + + private string Pattern { [DebuggerStepThrough] get; } + + private bool CaseSensitive { [DebuggerStepThrough] get; } + + protected override Task 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"); + } +} diff --git a/certmgr/Core/Filters/Impl/RegexStringFilter.cs b/certmgr/Core/Filters/Impl/RegexStringFilter.cs new file mode 100644 index 0000000..a7e23aa --- /dev/null +++ b/certmgr/Core/Filters/Impl/RegexStringFilter.cs @@ -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 +{ + internal RegexStringFilter(string regexPattern, bool caseSensitive) + { + Regex = new Regex(regexPattern, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase); + } + + private Regex Regex { [DebuggerStepThrough] get; } + + protected override Task 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"); + } +} diff --git a/certmgr/Core/Filters/Impl/WildcardStringFilter.cs b/certmgr/Core/Filters/Impl/WildcardStringFilter.cs new file mode 100644 index 0000000..8356ff8 --- /dev/null +++ b/certmgr/Core/Filters/Impl/WildcardStringFilter.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; +using System.Text.RegularExpressions; + +namespace CertMgr.Core.Filters.Impl; + +internal sealed class WildcardStringFilter : Filter +{ + 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 DoIsMatchAsync(string value, CancellationToken cancellationToken) + { + bool ismatch = Regex.IsMatch(value); + return Task.FromResult(ismatch); + } +} diff --git a/certmgr/Core/Filters/KnownFilters.cs b/certmgr/Core/Filters/KnownFilters.cs new file mode 100644 index 0000000..c4496a1 --- /dev/null +++ b/certmgr/Core/Filters/KnownFilters.cs @@ -0,0 +1,24 @@ +using CertMgr.Core.Filters.Impl; + +namespace CertMgr.Core.Filters; + +public static class KnownFilters +{ + public static class String + { + public static IFilter ExactMatch(string pattern, bool caseSensitive) + { + return new ExactMatchStringFilter(pattern, caseSensitive); + } + + public static IFilter Wildcard(string pattern, bool caseSensitive) + { + return new WildcardStringFilter(pattern, caseSensitive); + } + + public static IFilter Regex(string pattern, bool caseSensitive) + { + return new RegexStringFilter(pattern, caseSensitive); + } + } +} diff --git a/certmgr/Core/Filters/StringFilterType.cs b/certmgr/Core/Filters/StringFilterType.cs new file mode 100644 index 0000000..e2187a0 --- /dev/null +++ b/certmgr/Core/Filters/StringFilterType.cs @@ -0,0 +1,8 @@ +namespace CertMgr.Core.Filters; + +public enum StringFilterType +{ + ExactMatch = 1, + Wildcard, + Regex +} diff --git a/certmgr/Jobs/CertificateFilterConverter.cs b/certmgr/Jobs/CertificateFilterConverter.cs index f64a0ef..5f3f47a 100644 --- a/certmgr/Jobs/CertificateFilterConverter.cs +++ b/certmgr/Jobs/CertificateFilterConverter.cs @@ -26,12 +26,34 @@ internal sealed class CertificateFilterConverter : ValueConverter filterTypeSpan = span.Slice(0, separatorIndex); if (Enum.TryParse(filterTypeSpan, true, out CertificateFilterType filterType) && Enum.IsDefined(filterType)) { - ReadOnlySpan nameSpan = span.Slice(separatorIndex + 1, span.Length - separatorIndex - 1); - string value = nameSpan.ToString(); + ReadOnlySpan valueSpan = span.Slice(separatorIndex + 1, span.Length - separatorIndex - 1); + separatorIndex = valueSpan.IndexOf(':'); + + string pattern; + FilteringFlags flags; + + if (separatorIndex == -1) + { + pattern = valueSpan.ToString(); + flags = FilteringFlags.Exact; + } + else + { + ReadOnlySpan flagsSpan = valueSpan.Slice(0, separatorIndex); + flags = GetFilteringFlags(flagsSpan); + pattern = valueSpan.Slice(separatorIndex + 1).ToString(); + } + switch (filterType) { 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; default: break; @@ -39,7 +61,7 @@ internal sealed class CertificateFilterConverter : ValueConverter().ToSeparatedList()); + CLog.Error("Failed to parse filter type from value '{0}'. (available values = {1})", rawValue, Enum.GetNames().ToSeparatedList()); } } else @@ -49,4 +71,21 @@ internal sealed class CertificateFilterConverter : ValueConverter flagsSpan) + { + FilteringFlags flags = FilteringFlags.None; + + MemoryExtensions.SpanSplitEnumerator enu = flagsSpan.Split(','); + while (enu.MoveNext()) + { + ReadOnlySpan currentFlag = flagsSpan[enu.Current].Trim(); + if (Enum.TryParse(currentFlag, true, out FilteringFlags tmp) && Enum.IsDefined(tmp)) + { + flags |= tmp; + } + } + + return flags; + } } diff --git a/certmgr/Jobs/GetCertificateInfoJob.cs b/certmgr/Jobs/GetCertificateInfoJob.cs index 5c7079c..6023a64 100644 --- a/certmgr/Jobs/GetCertificateInfoJob.cs +++ b/certmgr/Jobs/GetCertificateInfoJob.cs @@ -14,7 +14,7 @@ public sealed class GetCertificateInfoJob : Job protected override async Task DoExecuteAsync(CancellationToken cancellationToken) { - X509Certificate2? cert = null; + List certs = new List(); if (!string.IsNullOrEmpty(Settings.File)) { @@ -22,7 +22,8 @@ public sealed class GetCertificateInfoJob : Job { try { - cert = X509CertificateLoader.LoadPkcs12FromFile(Settings.File, Settings.Password); + X509Certificate2 cert = X509CertificateLoader.LoadPkcs12FromFile(Settings.File, Settings.Password); + certs.Add(cert); } catch (Exception e) { @@ -39,8 +40,8 @@ public sealed class GetCertificateInfoJob : Job if (Settings.Filter != null) { CertStoreSearcher searcher = new CertStoreSearcher(Settings.Store); - IReadOnlyList certs = await searcher.SearchAsync(Settings.Filter, cancellationToken).ConfigureAwait(false); - cert = certs.FirstOrDefault(); + IReadOnlyList searchResult = await searcher.SearchAsync(Settings.Filter, cancellationToken).ConfigureAwait(false); + certs.AddRange(searchResult); } else { @@ -52,42 +53,48 @@ public sealed class GetCertificateInfoJob : Job 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; - sb.AppendLine(); - 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); - 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)) + + foreach (X509Certificate2 cert in certs) { - AppendWithPadding(sb, "FriendlyName", cert.FriendlyName, padSize); - } - 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) + int padSize = 24; + sb.AppendLine(); + 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); + 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, ext.Oid?.FriendlyName ?? ext.Oid?.Value, string.Empty, padSize); - foreach (string value in values) + AppendWithPadding(sb, "FriendlyName", cert.FriendlyName, padSize); + } + 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 - { - AppendWithPadding(sb, ext.Oid?.FriendlyName ?? ext.Oid?.Value, values[0], padSize); - } + + sb.Append("----------------------------------------------------------------"); } return CreateSuccess(sb.ToString()); diff --git a/certmgr/Program.cs b/certmgr/Program.cs index aac4ffe..706cf1a 100644 --- a/certmgr/Program.cs +++ b/certmgr/Program.cs @@ -14,7 +14,7 @@ internal static class Program args = [ "--job=get-certificate-info", "--cert-store=user/my", - "--filter=thumbprint:b395149b6079553eea92d9a73ffc97463e8976ff" + "--filter=thumbprint:regex:b39.+" ]; // args = [