< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Certificates.CertificateManager
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Certificates/CertificateManager.cs
Tag: Kestrun/Kestrun@5f1d2b981c9d7292c11fd448428c6ab6c811c5de
Line coverage
61%
Covered lines: 567
Uncovered lines: 355
Coverable lines: 922
Total lines: 2413
Line coverage: 61.4%
Branch coverage
57%
Covered branches: 227
Total branches: 392
Branch coverage: 57.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 11/19/2025 - 17:40:50 Line coverage: 52.7% (301/571) Branch coverage: 47.3% (107/226) Total lines: 1529 Tag: Kestrun/Kestrun@fcf33342333cef0516fe0d0912a86709874fd02612/15/2025 - 02:23:46 Line coverage: 53% (300/566) Branch coverage: 47.3% (107/226) Total lines: 1529 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c833002/18/2026 - 08:33:07 Line coverage: 53.7% (321/597) Branch coverage: 47.2% (118/250) Total lines: 1604 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b103/26/2026 - 03:54:59 Line coverage: 53.4% (322/602) Branch coverage: 47.2% (118/250) Total lines: 1604 Tag: Kestrun/Kestrun@844b5179fb0492dc6b1182bae3ff65fa7365521d04/19/2026 - 15:52:57 Line coverage: 60.9% (553/908) Branch coverage: 57.7% (224/388) Total lines: 2386 Tag: Kestrun/Kestrun@765a8f13c573c01494250a29d6392b6037f087c904/20/2026 - 15:01:11 Line coverage: 60.9% (553/908) Branch coverage: 57.7% (224/388) Total lines: 2388 Tag: Kestrun/Kestrun@633f156f2bdb11c688cfcba5b3f073a53ed5285804/23/2026 - 14:35:41 Line coverage: 61.4% (567/922) Branch coverage: 57.9% (227/392) Total lines: 2413 Tag: Kestrun/Kestrun@2fdbb120ca2faaa9acf2b8d2a34a7d64b067edbe 11/19/2025 - 17:40:50 Line coverage: 52.7% (301/571) Branch coverage: 47.3% (107/226) Total lines: 1529 Tag: Kestrun/Kestrun@fcf33342333cef0516fe0d0912a86709874fd02612/15/2025 - 02:23:46 Line coverage: 53% (300/566) Branch coverage: 47.3% (107/226) Total lines: 1529 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c833002/18/2026 - 08:33:07 Line coverage: 53.7% (321/597) Branch coverage: 47.2% (118/250) Total lines: 1604 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b103/26/2026 - 03:54:59 Line coverage: 53.4% (322/602) Branch coverage: 47.2% (118/250) Total lines: 1604 Tag: Kestrun/Kestrun@844b5179fb0492dc6b1182bae3ff65fa7365521d04/19/2026 - 15:52:57 Line coverage: 60.9% (553/908) Branch coverage: 57.7% (224/388) Total lines: 2386 Tag: Kestrun/Kestrun@765a8f13c573c01494250a29d6392b6037f087c904/20/2026 - 15:01:11 Line coverage: 60.9% (553/908) Branch coverage: 57.7% (224/388) Total lines: 2388 Tag: Kestrun/Kestrun@633f156f2bdb11c688cfcba5b3f073a53ed5285804/23/2026 - 14:35:41 Line coverage: 61.4% (567/922) Branch coverage: 57.9% (227/392) Total lines: 2413 Tag: Kestrun/Kestrun@2fdbb120ca2faaa9acf2b8d2a34a7d64b067edbe

Coverage delta

Coverage delta 11 -11

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()57.07%1166.67%
get_ShouldAppendKeyToPem()50%22100%
NewSelfSigned(...)100%22100%
CreateSelfSignedCertificate(...)83.33%303096.72%
CreateDevelopmentCertificate(...)100%44100%
NewCertificateRequest(...)83.33%171684.93%
Add()100%22100%
Import(...)83.33%121288.89%
ValidateImportInputs(...)100%88100%
ImportPfx(...)100%11100%
ImportDer(...)100%11100%
ImportPem(...)83.33%66100%
ImportPemUnencrypted(...)100%11100%
ImportPemEncrypted(...)25%6450%
TryManualEncryptedPemPairing(...)0%2040%
ExtractEncryptedPemDer(...)0%110100%
TryPairCertificateWithKey(...)0%4260%
TryPairWithRsa(...)0%2040%
TryPairWithEcdsa(...)0%2040%
LoadCertOnlyPem(...)50%4485.71%
Import(...)100%11100%
Import(...)100%11100%
Import(...)100%11100%
Export(...)75%4488.89%
NormalizeExportPath(...)35.71%721433.33%
EnsureOutputDirectoryExists(...)75%5460%
.ctor(...)100%11100%
get_Secure()100%11100%
get_Chars()100%11100%
Dispose()100%44100%
CreatePasswordShapes(...)100%44100%
ExportPfx(...)100%11100%
ExportPem(...)50%231050%
WritePrivateKey(...)35%642052%
Export(...)50%2287.5%
ExportPemFromJwkJson(...)0%620%
ExportPemFromJwkJson(...)0%620%
CreateSelfSignedCertificateFromJwk(...)0%2040%
BuildPrivateKeyJwt(...)100%210%
BuildPrivateKeyJwt(...)100%210%
BuildPrivateKeyJwtFromJwkJson(...)100%210%
CreateJwkJsonFromCertificate(...)0%7280%
CreateJwkJsonFromRsa(...)0%156120%
CreateJwkJsonFromRsaPrivateKeyPem(...)0%620%
Validate(...)100%11100%
Validate(...)100%210%
Validate(...)100%210%
Validate(...)77.78%191884.38%
IsWithinValidityPeriod(...)50%66100%
BuildChainOk(...)33.33%401242.11%
BuildChainOk(...)100%22100%
CreateValidationChain(...)50%22100%
ConfigureValidationChainPolicy(...)90%101093.33%
GetRootCertificates(...)100%66100%
GetIntermediateCertificates(...)83.33%6680%
CertificateMatches(...)100%11100%
IsSelfSignedCertificate(...)100%11100%
BuildChainFailureReason(...)50%44100%
PurposesOk(...)80%1010100%
NormalizeExpectedPurposeOids(...)100%11100%
GetEkuOids(...)75%8890%
UsesWeakAlgorithms(...)100%11100%
GetWeakAlgorithmFindings(...)55%232080%
DescribeCertificate(...)100%11100%
DescribeExpectedPurposes(...)100%44100%
DescribeChainStatuses(...)50%44100%
ResolveIssuerMaterial(...)100%22100%
ValidateIssuerCertificate(...)80%111077.78%
GetPrivateKeyParameter(...)25%10427.27%
GetPrivateKeyParameter(...)100%1150%
GetPrivateKeyParameter(...)100%210%
CreateDevelopmentRoot(...)100%11100%
TrustDevelopmentRootIfRequested(...)16.67%27616.67%
CreatePublicOnlyCertificate(...)100%11100%
ValidateDevelopmentCertificateOptions(...)50%211260%
ResolveEnhancedKeyUsages(...)75%4488.89%
ResolveKeyUsage(...)100%88100%
GetSignatureAlgorithm(...)75%4483.33%
GetPurposes(...)50%22100%
GenRsaKeyPair(...)100%11100%
GenEcKeyPair(...)33.33%6681.82%
ToX509Cert2(...)100%2274.19%
EnsureExportablePrivateKeySupport(...)83.33%14640%
SupportsPkcs8PrivateKeyExport(...)75%5463.64%
.ctor(...)100%11100%
get_Subject()100%11100%
get_PrivateKey()100%11100%
get_PublicKeyInfo()100%11100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Certificates/CertificateManager.cs

#LineLine coverage
 1using System.Net;
 2using System.Security;
 3using System.Security.Cryptography;
 4using System.Security.Cryptography.X509Certificates;
 5using Org.BouncyCastle.Asn1;
 6using Org.BouncyCastle.Asn1.Pkcs;
 7using Org.BouncyCastle.Asn1.X509;
 8using Org.BouncyCastle.Crypto;
 9using Org.BouncyCastle.Crypto.Generators;
 10using Org.BouncyCastle.Crypto.Operators;
 11using Org.BouncyCastle.Crypto.Parameters;
 12using Org.BouncyCastle.Crypto.Prng;
 13using Org.BouncyCastle.Math;
 14using Org.BouncyCastle.OpenSsl;
 15using Org.BouncyCastle.Pkcs;
 16using Org.BouncyCastle.Security;
 17using Org.BouncyCastle.Utilities;
 18using Org.BouncyCastle.X509;
 19using Org.BouncyCastle.X509.Extension;
 20using System.Text;
 21using Org.BouncyCastle.Asn1.X9;
 22using Serilog;
 23using Kestrun.Utilities;
 24using System.Text.Json;
 25using Microsoft.IdentityModel.Tokens;
 26using System.Security.Claims;
 27using Microsoft.IdentityModel.JsonWebTokens;
 28using System.Text.Json.Serialization;
 29
 30namespace Kestrun.Certificates;
 31
 32/// <summary>
 33/// Drop-in replacement for Pode’s certificate helpers, powered by Bouncy Castle.
 34/// </summary>
 35public static class CertificateManager
 36{
 137    private static readonly string[] DefaultDevelopmentDnsNames = ["localhost", "127.0.0.1", "::1"];
 38
 39    /// <summary>
 40    /// Controls whether the private key is appended to the certificate PEM file in addition to
 41    /// writing a separate .key file. Appending was initially added to work around platform
 42    /// inconsistencies when importing encrypted PEM pairs on some Linux runners. However, having
 43    /// both a combined (cert+key) file and a separate key file can itself introduce ambiguity in
 44    /// which API path <see cref="X509Certificate2"/> chooses (single-file vs dual-file), which was
 45    /// observed to contribute to rare flakiness (private key occasionally not attached after
 46    /// import). To make behavior deterministic we now disable appending by default and allow it to
 47    /// be re-enabled explicitly via the environment variable KESTRUN_APPEND_KEY_TO_PEM.
 48    /// Set KESTRUN_APPEND_KEY_TO_PEM=1 (or "true") to re-enable.
 49    /// </summary>
 50    private static bool ShouldAppendKeyToPem =>
 251        string.Equals(Environment.GetEnvironmentVariable("KESTRUN_APPEND_KEY_TO_PEM"), "1", StringComparison.OrdinalIgno
 252        string.Equals(Environment.GetEnvironmentVariable("KESTRUN_APPEND_KEY_TO_PEM"), "true", StringComparison.OrdinalI
 53
 54    #region  Self-signed certificate
 55
 56    /// <summary>
 57    /// Creates a new self-signed certificate based on the provided options. If the Development flag is set,
 58    /// a development certificate bundle (including a root and leaf certificate) will be created; otherwise, a single se
 59    /// </summary>
 60    /// <param name="o">Options for creating the self-signed certificate.</param>
 61    /// <returns>A result containing the generated certificate and any development-bundle metadata.</returns>
 62    public static SelfSignedCertificateResult NewSelfSigned(SelfSignedOptions o)
 63    {
 2664        ArgumentNullException.ThrowIfNull(o);
 65
 2666        return o.Development
 2667            ? CreateDevelopmentCertificate(o)
 2668            : new SelfSignedCertificateResult(CreateSelfSignedCertificate(o));
 69    }
 70
 71    /// <summary>
 72    /// Creates a single self-signed X509 certificate using the specified options.
 73    /// </summary>
 74    /// <param name="o">Options for creating the self-signed certificate.</param>
 75    /// <returns>A new self-signed X509Certificate2 instance.</returns>
 76    private static X509Certificate2 CreateSelfSignedCertificate(SelfSignedOptions o)
 77    {
 2878        var random = new SecureRandom(new CryptoApiRandomGenerator());
 2879        var normalizedDnsNames = (o.DnsNames ?? [])
 6780            .Select(name => name?.Trim())
 6781            .Where(name => !string.IsNullOrWhiteSpace(name))
 2882            .Distinct(StringComparer.OrdinalIgnoreCase)
 2883            .ToArray();
 84
 2885        if (normalizedDnsNames.Length == 0)
 86        {
 087            throw new ArgumentException("At least one non-empty DNS name, IP address, or subject name value is required.
 88        }
 89
 90        // ── 1. Key pair ───────────────────────────────────────────────────────────
 2891        var keyPair =
 2892            o.KeyType switch
 2893            {
 2794                KeyType.Rsa => GenRsaKeyPair(o.KeyLength, random),
 195                KeyType.Ecdsa => GenEcKeyPair(o.KeyLength, random),
 096                _ => throw new ArgumentOutOfRangeException()
 2897            };
 98
 99        // ── 2. Certificate body ───────────────────────────────────────────────────
 28100        var notBefore = DateTime.UtcNow.AddMinutes(-5);
 28101        var notAfter = notBefore.AddDays(o.ValidDays);
 28102        var serial = BigIntegers.CreateRandomInRange(
 28103                            BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
 104
 28105        var subjectDn = new X509Name($"CN={normalizedDnsNames[0]}");
 28106        var issuerMaterial = ResolveIssuerMaterial(o.IssuerCertificate);
 27107        var gen = new X509V3CertificateGenerator();
 27108        gen.SetSerialNumber(serial);
 27109        gen.SetIssuerDN(issuerMaterial?.Subject ?? subjectDn);
 27110        gen.SetSubjectDN(subjectDn);
 27111        gen.SetNotBefore(notBefore);
 27112        gen.SetNotAfter(notAfter);
 27113        gen.SetPublicKey(keyPair.Public);
 114
 27115        if (!o.IsCertificateAuthority)
 116        {
 117            // SANs are relevant for leaf server/client certificates, not CA certificates.
 22118            var altNames = normalizedDnsNames
 22119                            .Select(sanValue =>
 58120                                new GeneralName(
 58121                                    IPAddress.TryParse(sanValue, out _)
 58122                                        ? GeneralName.IPAddress
 58123                                        : GeneralName.DnsName,
 58124                                    sanValue))
 22125                            .ToArray();
 22126            gen.AddExtension(X509Extensions.SubjectAlternativeName, false,
 22127                             new DerSequence(altNames));
 128        }
 129
 27130        gen.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(o.IsCertificateAuthority));
 131
 27132        var subjectPublicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(keyPair.Public);
 27133        var subjectKeyIdentifier = X509ExtensionUtilities.CreateSubjectKeyIdentifier(subjectPublicKeyInfo);
 27134        gen.AddExtension(X509Extensions.SubjectKeyIdentifier, false, subjectKeyIdentifier);
 27135        gen.AddExtension(
 27136            X509Extensions.AuthorityKeyIdentifier,
 27137            false,
 27138            X509ExtensionUtilities.CreateAuthorityKeyIdentifier(issuerMaterial?.PublicKeyInfo ?? subjectPublicKeyInfo));
 139
 27140        var eku = ResolveEnhancedKeyUsages(o);
 27141        if (eku.Length > 0)
 142        {
 22143            gen.AddExtension(X509Extensions.ExtendedKeyUsage, false,
 22144                             new ExtendedKeyUsage(eku));
 145        }
 146
 27147        var keyUsage = ResolveKeyUsage(o);
 27148        gen.AddExtension(X509Extensions.KeyUsage, true,
 27149                         new KeyUsage(keyUsage));
 150
 151        // ── 3. Sign & output ──────────────────────────────────────────────────────
 27152        var signingKey = issuerMaterial?.PrivateKey ?? keyPair.Private;
 27153        var sigAlg = GetSignatureAlgorithm(signingKey);
 27154        var signer = new Asn1SignatureFactory(sigAlg, signingKey, random);
 27155        var cert = gen.Generate(signer);
 156
 27157        return ToX509Cert2(cert, keyPair.Private,
 27158            o.Exportable ? X509KeyStorageFlags.Exportable : X509KeyStorageFlags.DefaultKeySet,
 27159            o.Ephemeral);
 160    }
 161
 162    /// <summary>
 163    /// Creates a development certificate bundle consisting of a CA root certificate and an issued leaf certificate.
 164    /// </summary>
 165    /// <param name="options">Options controlling development certificate creation and optional root trust.</param>
 166    /// <returns>A result containing the effective root certificate, issued leaf certificate, and whether the root certi
 167    private static SelfSignedCertificateResult CreateDevelopmentCertificate(SelfSignedOptions options)
 168    {
 3169        ValidateDevelopmentCertificateOptions(options);
 170
 3171        var effectiveRoot = options.RootCertificate ?? CreateDevelopmentRoot(options);
 3172        var rootTrusted = TrustDevelopmentRootIfRequested(options, effectiveRoot);
 3173        var dnsNames = options.DnsNames ?? DefaultDevelopmentDnsNames;
 3174        var leaf = CreateSelfSignedCertificate(new SelfSignedOptions(
 3175            dnsNames,
 3176            KeyType: options.KeyType,
 3177            KeyLength: options.KeyLength,
 3178            Purposes: options.Purposes,
 3179            KeyUsageFlags: options.KeyUsageFlags,
 3180            ValidDays: options.LeafValidDays,
 3181            Ephemeral: options.Ephemeral,
 3182            Exportable: options.Exportable,
 3183            IssuerCertificate: effectiveRoot));
 184
 3185        return new SelfSignedCertificateResult(leaf)
 3186        {
 3187            RootCertificate = effectiveRoot,
 3188            LeafCertificate = leaf,
 3189            RootTrusted = rootTrusted,
 3190            PublicRootCertificate = CreatePublicOnlyCertificate(effectiveRoot)
 3191        };
 192    }
 193    #endregion
 194
 195    #region  CSR
 196
 197    /// <summary>
 198    /// Creates a new Certificate Signing Request (CSR) and returns the PEM-encoded CSR and the private key.
 199    /// </summary>
 200    /// <param name="options">The options for the CSR.</param>
 201    /// <param name="encryptionPassword">The password to encrypt the private key, if desired.</param>
 202    /// <returns>A <see cref="CsrResult"/> containing the CSR and private key information.</returns>
 203    /// <exception cref="ArgumentOutOfRangeException"></exception>
 204    public static CsrResult NewCertificateRequest(CsrOptions options, ReadOnlySpan<char> encryptionPassword = default)
 205    {
 206        // 0️⃣ Keypair
 3207        var random = new SecureRandom(new CryptoApiRandomGenerator());
 3208        var keyPair = options.KeyType switch
 3209        {
 2210            KeyType.Rsa => GenRsaKeyPair(options.KeyLength, random),
 1211            KeyType.Ecdsa => GenEcKeyPair(options.KeyLength, random),
 0212            _ => throw new ArgumentOutOfRangeException(nameof(options.KeyType))
 3213        };
 214
 215        // 1️⃣ Subject DN
 3216        var order = new List<DerObjectIdentifier>();
 3217        var attrs = new Dictionary<DerObjectIdentifier, string>();
 218        void Add(DerObjectIdentifier oid, string? v)
 219        {
 18220            if (!string.IsNullOrWhiteSpace(v)) { order.Add(oid); attrs[oid] = v; }
 12221        }
 3222        Add(X509Name.C, options.Country);
 3223        Add(X509Name.O, options.Org);
 3224        Add(X509Name.OU, options.OrgUnit);
 3225        Add(X509Name.CN, options.CommonName ?? options.DnsNames.First());
 3226        var subject = new X509Name(order, attrs);
 227
 228        // 2️⃣ SAN extension
 3229        var altNames = options.DnsNames
 4230            .Select(d => new GeneralName(
 4231                IPAddress.TryParse(d, out _)
 4232                    ? GeneralName.IPAddress
 4233                    : GeneralName.DnsName, d))
 3234            .ToArray();
 3235        var sanSeq = new DerSequence(altNames);
 236
 3237        var extGen = new X509ExtensionsGenerator();
 3238        extGen.AddExtension(X509Extensions.SubjectAlternativeName, false, sanSeq);
 239
 3240        if (options.KeyUsageFlags is { } keyUsageFlags && keyUsageFlags != X509KeyUsageFlags.None)
 241        {
 1242            extGen.AddExtension(X509Extensions.KeyUsage, true, new KeyUsage((int)keyUsageFlags));
 243        }
 244
 3245        var extensions = extGen.Generate();
 246
 3247        var extensionRequestAttr = new AttributePkcs(
 3248            PkcsObjectIdentifiers.Pkcs9AtExtensionRequest,
 3249            new DerSet(extensions));
 3250        var attrSet = new DerSet(extensionRequestAttr);
 251
 252        // 3️⃣ CSR
 3253        var sigAlg = options.KeyType == KeyType.Rsa ? "SHA256WITHRSA" : "SHA384WITHECDSA";
 3254        var csr = new Pkcs10CertificationRequest(sigAlg, subject, keyPair.Public, attrSet, keyPair.Private);
 255
 256        // 4️⃣ CSR PEM + DER
 257        string csrPem;
 3258        using (var sw = new StringWriter())
 259        {
 3260            new PemWriter(sw).WriteObject(csr);
 3261            csrPem = sw.ToString();
 3262        }
 3263        var csrDer = csr.GetEncoded();
 264
 265        // 5️⃣ Private key PEM + DER
 266        string privateKeyPem;
 3267        using (var sw = new StringWriter())
 268        {
 3269            new PemWriter(sw).WriteObject(keyPair.Private);
 3270            privateKeyPem = sw.ToString();
 3271        }
 3272        var pkInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(keyPair.Private);
 3273        var privateKeyDer = pkInfo.GetEncoded();
 274
 275        // 6️⃣ Optional encrypted PEM
 3276        string? privateKeyPemEncrypted = null;
 3277        if (!encryptionPassword.IsEmpty)
 278        {
 0279            var pwd = encryptionPassword.ToArray(); // BC requires char[]
 280            try
 281            {
 0282                var gen = new Pkcs8Generator(keyPair.Private, Pkcs8Generator.PbeSha1_3DES)
 0283                {
 0284                    Password = pwd
 0285                };
 0286                using var encSw = new StringWriter();
 0287                new PemWriter(encSw).WriteObject(gen);
 0288                privateKeyPemEncrypted = encSw.ToString();
 289            }
 290            finally
 291            {
 0292                Array.Clear(pwd, 0, pwd.Length); // wipe memory
 0293            }
 294        }
 295
 296        // 7️⃣ Public key PEM + DER
 3297        var spki = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(keyPair.Public);
 3298        var publicKeyDer = spki.GetEncoded();
 299        string publicKeyPem;
 3300        using (var sw = new StringWriter())
 301        {
 3302            new PemWriter(sw).WriteObject(spki);
 3303            publicKeyPem = sw.ToString();
 3304        }
 305
 3306        return new CsrResult(
 3307            csrPem,
 3308            csrDer,
 3309            keyPair.Private,
 3310            privateKeyPem,
 3311            privateKeyDer,
 3312            privateKeyPemEncrypted,
 3313            publicKeyPem,
 3314            publicKeyDer
 3315        );
 316    }
 317
 318    #endregion
 319
 320    #region  Import
 321    /// <summary>
 322    /// Imports an X509 certificate from the specified file path, with optional password and private key file.
 323    /// </summary>
 324    /// <param name="certPath">The path to the certificate file.</param>
 325    /// <param name="password">The password for the certificate, if required.</param>
 326    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 327    /// <param name="flags">Key storage flags for the imported certificate.</param>
 328    /// <returns>The imported X509Certificate2 instance.</returns>
 329    public static X509Certificate2 Import(
 330       string certPath,
 331       ReadOnlySpan<char> password = default,
 332       string? privateKeyPath = null,
 333       X509KeyStorageFlags flags = X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.Exportable)
 334    {
 9335        ValidateImportInputs(certPath, privateKeyPath);
 336
 6337        var ext = Path.GetExtension(certPath).ToLowerInvariant();
 6338        return ext switch
 6339        {
 2340            ".pfx" or ".p12" => ImportPfx(certPath, password, flags),
 1341            ".cer" or ".der" => ImportDer(certPath),
 3342            ".pem" or ".crt" => ImportPem(certPath, password, privateKeyPath),
 0343            _ => throw new NotSupportedException($"Certificate extension '{ext}' is not supported.")
 6344        };
 345    }
 346
 347    /// <summary>
 348    /// Validates the inputs for importing a certificate.
 349    /// </summary>
 350    /// <param name="certPath">The path to the certificate file.</param>
 351    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 352    private static void ValidateImportInputs(string certPath, string? privateKeyPath)
 353    {
 9354        if (string.IsNullOrEmpty(certPath))
 355        {
 1356            throw new ArgumentException("Certificate path cannot be null or empty.", nameof(certPath));
 357        }
 8358        if (!File.Exists(certPath))
 359        {
 1360            throw new FileNotFoundException("Certificate file not found.", certPath);
 361        }
 7362        if (!string.IsNullOrEmpty(privateKeyPath) && !File.Exists(privateKeyPath))
 363        {
 1364            throw new FileNotFoundException("Private key file not found.", privateKeyPath);
 365        }
 6366    }
 367
 368    /// <summary>
 369    /// Imports a PFX certificate from the specified file path.
 370    /// </summary>
 371    /// <param name="certPath">The path to the certificate file.</param>
 372    /// <param name="password">The password for the certificate, if required.</param>
 373    /// <param name="flags">Key storage flags for the imported certificate.</param>
 374    /// <returns>The imported X509Certificate2 instance.</returns>
 375    private static X509Certificate2 ImportPfx(string certPath, ReadOnlySpan<char> password, X509KeyStorageFlags flags)
 376#if NET9_0_OR_GREATER
 2377        => X509CertificateLoader.LoadPkcs12FromFile(certPath, password, flags, Pkcs12LoaderLimits.Defaults);
 378#else
 379        => new(File.ReadAllBytes(certPath), password, flags);
 380#endif
 381
 382    private static X509Certificate2 ImportDer(string certPath)
 383#if NET9_0_OR_GREATER
 1384        => X509CertificateLoader.LoadCertificateFromFile(certPath);
 385#else
 386        => new(File.ReadAllBytes(certPath));
 387#endif
 388
 389    /// <summary>
 390    /// Imports a PEM certificate from the specified file path.
 391    /// </summary>
 392    /// <param name="certPath">The path to the certificate file.</param>
 393    /// <param name="password">The password for the certificate, if required.</param>
 394    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 395    /// <returns>The imported X509Certificate2 instance.</returns>
 396    private static X509Certificate2 ImportPem(string certPath, ReadOnlySpan<char> password, string? privateKeyPath)
 397    {
 398        // No separate key file provided
 3399        if (string.IsNullOrEmpty(privateKeyPath))
 400        {
 1401            return password.IsEmpty
 1402                ? LoadCertOnlyPem(certPath)
 1403                : X509Certificate2.CreateFromEncryptedPemFile(certPath, password);
 404        }
 405
 406        // Separate key file provided
 2407        return password.IsEmpty
 2408            ? ImportPemUnencrypted(certPath, privateKeyPath)
 2409            : ImportPemEncrypted(certPath, password, privateKeyPath);
 410    }
 411
 412    /// <summary>
 413    /// Imports an unencrypted PEM certificate from the specified file path.
 414    /// </summary>
 415    /// <param name="certPath">The path to the certificate file.</param>
 416    /// <param name="privateKeyPath">The path to the private key file.</param>
 417    /// <returns>The imported X509Certificate2 instance.</returns>
 418    private static X509Certificate2 ImportPemUnencrypted(string certPath, string privateKeyPath)
 1419        => X509Certificate2.CreateFromPemFile(certPath, privateKeyPath);
 420
 421    /// <summary>
 422    /// Imports a PEM certificate from the specified file path.
 423    /// </summary>
 424    /// <param name="certPath">The path to the certificate file.</param>
 425    /// <param name="password">The password for the certificate, if required.</param>
 426    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 427    /// <returns>The imported X509Certificate2 instance.</returns>
 428    private static X509Certificate2 ImportPemEncrypted(string certPath, ReadOnlySpan<char> password, string privateKeyPa
 429    {
 430        // Prefer single-file path (combined) first for reliability on some platforms
 431        try
 432        {
 1433            var single = X509Certificate2.CreateFromEncryptedPemFile(certPath, password);
 0434            if (single.HasPrivateKey)
 435            {
 0436                Log.Debug("Imported encrypted PEM using single-file path (combined cert+key) for {CertPath}", certPath);
 0437                return single;
 438            }
 0439        }
 1440        catch (Exception exSingle)
 441        {
 1442            Log.Debug(exSingle, "Single-file encrypted PEM import failed, falling back to separate key file {KeyFile}", 
 1443        }
 444
 1445        var loaded = X509Certificate2.CreateFromEncryptedPemFile(certPath, password, privateKeyPath);
 446
 1447        if (loaded.HasPrivateKey)
 448        {
 1449            return loaded;
 450        }
 451
 452        // Fallback manual pairing if platform failed to associate the key
 0453        TryManualEncryptedPemPairing(certPath, password, privateKeyPath, ref loaded);
 0454        return loaded;
 0455    }
 456
 457    /// <summary>
 458    /// Tries to manually pair an encrypted PEM certificate with its private key.
 459    /// </summary>
 460    /// <param name="certPath">The path to the certificate file.</param>
 461    /// <param name="password">The password for the certificate, if required.</param>
 462    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 463    /// <param name="loaded">The loaded X509Certificate2 instance.</param>
 464    private static void TryManualEncryptedPemPairing(string certPath, ReadOnlySpan<char> password, string privateKeyPath
 465    {
 466        try
 467        {
 0468            var certOnly = LoadCertOnlyPem(certPath);
 0469            var encDer = ExtractEncryptedPemDer(privateKeyPath);
 470
 0471            if (encDer is null)
 472            {
 0473                Log.Debug("Encrypted PEM manual pairing fallback skipped: markers not found in key file {KeyFile}", priv
 0474                return;
 475            }
 476
 0477            var lastErr = TryPairCertificateWithKey(certOnly, password, encDer, ref loaded);
 478
 0479            if (lastErr != null)
 480            {
 0481                Log.Debug(lastErr, "Encrypted PEM manual pairing attempts failed (all rounds); returning original loaded
 482            }
 0483        }
 0484        catch (Exception ex)
 485        {
 0486            Log.Debug(ex, "Encrypted PEM manual pairing fallback failed unexpectedly; returning original loaded certific
 0487        }
 0488    }
 489
 490    /// <summary>
 491    /// Extracts the encrypted PEM DER bytes from a private key file.
 492    /// </summary>
 493    /// <param name="privateKeyPath">The path to the private key file.</param>
 494    /// <returns>The DER bytes if successful, null otherwise.</returns>
 495    private static byte[]? ExtractEncryptedPemDer(string privateKeyPath)
 496    {
 497        const string encBegin = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
 498        const string encEnd = "-----END ENCRYPTED PRIVATE KEY-----";
 499
 0500        byte[]? encDer = null;
 0501        for (var attempt = 0; attempt < 5 && encDer is null; attempt++)
 502        {
 0503            var keyPem = File.ReadAllText(privateKeyPath);
 0504            var start = keyPem.IndexOf(encBegin, StringComparison.Ordinal);
 0505            var end = keyPem.IndexOf(encEnd, StringComparison.Ordinal);
 0506            if (start >= 0 && end > start)
 507            {
 0508                start += encBegin.Length;
 0509                var b64 = keyPem[start..end].Replace("\r", "").Replace("\n", "").Trim();
 0510                try { encDer = Convert.FromBase64String(b64); }
 0511                catch (FormatException fe)
 512                {
 0513                    Log.Debug(fe, "Base64 decode failed on attempt {Attempt} reading encrypted key; retrying", attempt +
 0514                }
 515            }
 0516            if (encDer is null)
 517            {
 0518                Thread.Sleep(40 * (attempt + 1));
 519            }
 520        }
 521
 0522        return encDer;
 523    }
 524
 525    /// <summary>
 526    /// Attempts to pair a certificate with an encrypted private key using RSA and ECDSA.
 527    /// </summary>
 528    /// <param name="certOnly">The certificate without a private key.</param>
 529    /// <param name="password">The password for the encrypted key.</param>
 530    /// <param name="encDer">The encrypted DER bytes.</param>
 531    /// <param name="loaded">The loaded certificate (updated if pairing succeeds).</param>
 532    /// <returns>The last exception encountered, or null if pairing succeeded.</returns>
 533    private static Exception? TryPairCertificateWithKey(X509Certificate2 certOnly, ReadOnlySpan<char> password, byte[] e
 534    {
 0535        Exception? lastErr = null;
 0536        for (var round = 0; round < 2; round++)
 537        {
 0538            if (TryPairWithRsa(certOnly, password, encDer, round, ref loaded, ref lastErr))
 539            {
 0540                return null;
 541            }
 542
 0543            if (TryPairWithEcdsa(certOnly, password, encDer, round, ref loaded, ref lastErr))
 544            {
 0545                return null;
 546            }
 547
 0548            Thread.Sleep(25 * (round + 1));
 549        }
 0550        return lastErr;
 551    }
 552
 553    /// <summary>
 554    /// Tries to pair a certificate with an RSA private key.
 555    /// </summary>
 556    /// <param name="certOnly">The certificate without a private key.</param>
 557    /// <param name="password">The password for the encrypted key.</param>
 558    /// <param name="encDer">The encrypted DER bytes.</param>
 559    /// <param name="round">The attempt round number.</param>
 560    /// <param name="loaded">The loaded certificate (updated if pairing succeeds).</param>
 561    /// <param name="lastErr">The last exception encountered (updated on failure).</param>
 562    /// <returns>True if pairing succeeded, false otherwise.</returns>
 563    private static bool TryPairWithRsa(X509Certificate2 certOnly, ReadOnlySpan<char> password, byte[] encDer, int round,
 564    {
 565        try
 566        {
 0567            using var rsa = RSA.Create();
 0568            rsa.ImportEncryptedPkcs8PrivateKey(password, encDer, out _);
 0569            var withKey = certOnly.CopyWithPrivateKey(rsa);
 0570            if (withKey.HasPrivateKey)
 571            {
 0572                Log.Debug("Encrypted PEM manual pairing succeeded with RSA private key (round {Round}).", round + 1);
 0573                loaded = withKey;
 0574                return true;
 575            }
 0576        }
 0577        catch (Exception exRsa)
 578        {
 0579            lastErr = lastErr is null ? exRsa : new AggregateException(lastErr, exRsa);
 0580        }
 0581        return false;
 0582    }
 583
 584    /// <summary>
 585    /// Tries to pair a certificate with an ECDSA private key.
 586    /// </summary>
 587    /// <param name="certOnly">The certificate without a private key.</param>
 588    /// <param name="password">The password for the encrypted key.</param>
 589    /// <param name="encDer">The encrypted DER bytes.</param>
 590    /// <param name="round">The attempt round number.</param>
 591    /// <param name="loaded">The loaded certificate (updated if pairing succeeds).</param>
 592    /// <param name="lastErr">The last exception encountered (updated on failure).</param>
 593    /// <returns>True if pairing succeeded, false otherwise.</returns>
 594    private static bool TryPairWithEcdsa(X509Certificate2 certOnly, ReadOnlySpan<char> password, byte[] encDer, int roun
 595    {
 596        try
 597        {
 0598            using var ecdsa = ECDsa.Create();
 0599            ecdsa.ImportEncryptedPkcs8PrivateKey(password, encDer, out _);
 0600            var withKey = certOnly.CopyWithPrivateKey(ecdsa);
 0601            if (withKey.HasPrivateKey)
 602            {
 0603                Log.Debug("Encrypted PEM manual pairing succeeded with ECDSA private key (round {Round}).", round + 1);
 0604                loaded = withKey;
 0605                return true;
 606            }
 0607        }
 0608        catch (Exception exEc)
 609        {
 0610            lastErr = lastErr is null ? exEc : new AggregateException(lastErr, exEc);
 0611        }
 0612        return false;
 0613    }
 614
 615    /// <summary>
 616    /// Loads a certificate from a PEM file that contains *only* a CERTIFICATE block (no key).
 617    /// </summary>
 618    /// <param name="certPath">The path to the certificate file.</param>
 619    /// <returns>The loaded X509Certificate2 instance.</returns>
 620    private static X509Certificate2 LoadCertOnlyPem(string certPath)
 621    {
 622        // 1) Read + trim the whole PEM text
 1623        var pem = File.ReadAllText(certPath).Trim();
 624
 625        // 2) Define the BEGIN/END markers
 626        const string begin = "-----BEGIN CERTIFICATE-----";
 627        const string end = "-----END CERTIFICATE-----";
 628
 629        // 3) Find their positions
 1630        var start = pem.IndexOf(begin, StringComparison.Ordinal);
 1631        if (start < 0)
 632        {
 0633            throw new InvalidDataException("BEGIN CERTIFICATE marker not found");
 634        }
 635
 1636        start += begin.Length;
 637
 1638        var stop = pem.IndexOf(end, start, StringComparison.Ordinal);
 1639        if (stop < 0)
 640        {
 0641            throw new InvalidDataException("END CERTIFICATE marker not found");
 642        }
 643
 644        // 4) Extract, clean, and decode the Base64 payload
 1645        var b64 = pem[start..stop]
 1646                       .Replace("\r", "")
 1647                       .Replace("\n", "")
 1648                       .Trim();
 1649        var der = Convert.FromBase64String(b64);
 650
 651        // 5) Return the X509Certificate2
 652
 653#if NET9_0_OR_GREATER
 1654        return X509CertificateLoader.LoadCertificate(der);
 655#else
 656        // .NET 8 or earlier path, using X509Certificate2 ctor
 657        // Note: this will not work in .NET 9+ due to the new X509CertificateLoader API
 658        //       which requires a byte array or a file path.
 659        return new X509Certificate2(der);
 660#endif
 661    }
 662
 663    /// <summary>
 664    /// Imports an X509 certificate from the specified file path, using a SecureString password and optional private key
 665    /// </summary>
 666    /// <param name="certPath">The path to the certificate file.</param>
 667    /// <param name="password">The SecureString password for the certificate, if required.</param>
 668    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 669    /// <param name="flags">Key storage flags for the imported certificate.</param>
 670    /// <returns>The imported X509Certificate2 instance.</returns>
 671    public static X509Certificate2 Import(
 672       string certPath,
 673       SecureString password,
 674       string? privateKeyPath = null,
 675       X509KeyStorageFlags flags = X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.Exportable)
 676    {
 1677        X509Certificate2? result = null;
 1678        Log.Debug("Importing certificate from {CertPath} with flags {Flags}", certPath, flags);
 679        // ToSecureSpan zero-frees its buffer as soon as this callback returns.
 1680        password.ToSecureSpan(span =>
 1681        {
 1682            // capture the return value of the span-based overload
 1683            result = Import(certPath: certPath, password: span, privateKeyPath: privateKeyPath, flags: flags);
 2684        });
 685
 686        // at this point, unmanaged memory is already zeroed
 1687        return result!;   // non-null because the callback always runs exactly once
 688    }
 689
 690    /// <summary>
 691    /// Imports an X509 certificate from the specified file path, with optional private key file and key storage flags.
 692    /// </summary>
 693    /// <param name="certPath">The path to the certificate file.</param>
 694    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 695    /// <param name="flags">Key storage flags for the imported certificate.</param>
 696    /// <returns>The imported X509Certificate2 instance.</returns>
 697    public static X509Certificate2 Import(
 698         string certPath,
 699         string? privateKeyPath = null,
 700         X509KeyStorageFlags flags = X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.Exportable)
 701    {
 702        // ToSecureSpan zero-frees its buffer as soon as this callback returns.
 2703        ReadOnlySpan<char> passwordSpan = default;
 704        // capture the return value of the span-based overload
 2705        var result = Import(certPath: certPath, password: passwordSpan, privateKeyPath: privateKeyPath, flags: flags);
 1706        return result;
 707    }
 708
 709    /// <summary>
 710    /// Imports an X509 certificate from the specified file path.
 711    /// </summary>
 712    /// <param name="certPath">The path to the certificate file.</param>
 713    /// <returns>The imported X509Certificate2 instance.</returns>
 714    public static X509Certificate2 Import(string certPath)
 715    {
 716        // ToSecureSpan zero-frees its buffer as soon as this callback returns.
 4717        ReadOnlySpan<char> passwordSpan = default;
 718        // capture the return value of the span-based overload
 4719        var result = Import(certPath: certPath, password: passwordSpan);
 2720        return result;
 721    }
 722
 723    #endregion
 724
 725    #region Export
 726    /// <summary>
 727    /// Exports the specified X509 certificate to a file in the given format, with optional password and private key inc
 728    /// </summary>
 729    /// <param name="cert">The X509Certificate2 to export.</param>
 730    /// <param name="filePath">The file path to export the certificate to.</param>
 731    /// <param name="fmt">The export format (Pfx or Pem).</param>
 732    /// <param name="password">The password to protect the exported certificate or private key, if applicable.</param>
 733    /// <param name="includePrivateKey">Whether to include the private key in the export.</param>
 734    public static void Export(X509Certificate2 cert, string filePath, ExportFormat fmt,
 735           ReadOnlySpan<char> password = default, bool includePrivateKey = false)
 736    {
 737        // Normalize/validate target path and format
 4738        filePath = NormalizeExportPath(filePath, fmt);
 739
 740        // Ensure output directory exists
 4741        EnsureOutputDirectoryExists(filePath);
 742
 743        // Prepare password shapes once
 4744        using var shapes = CreatePasswordShapes(password);
 745
 746        switch (fmt)
 747        {
 748            case ExportFormat.Pfx:
 2749                ExportPfx(cert, filePath, shapes.Secure);
 2750                break;
 751            case ExportFormat.Pem:
 2752                ExportPem(cert, filePath, password, includePrivateKey);
 2753                break;
 754            default:
 0755                throw new NotSupportedException($"Unsupported export format: {fmt}");
 756        }
 4757    }
 758
 759    /// <summary>
 760    /// Normalizes the export file path based on the desired export format.
 761    /// </summary>
 762    /// <param name="filePath">The original file path.</param>
 763    /// <param name="fmt">The desired export format.</param>
 764    /// <returns>The normalized file path.</returns>
 765    private static string NormalizeExportPath(string filePath, ExportFormat fmt)
 766    {
 4767        var fileExtension = Path.GetExtension(filePath).ToLowerInvariant();
 768        switch (fileExtension)
 769        {
 770            case ".pfx":
 2771                if (fmt != ExportFormat.Pfx)
 772                {
 0773                    throw new NotSupportedException(
 0774                            $"File extension '{fileExtension}' for '{filePath}' is not supported for PFX certificates.")
 775                }
 776
 777                break;
 778            case ".pem":
 2779                if (fmt != ExportFormat.Pem)
 780                {
 0781                    throw new NotSupportedException(
 0782                            $"File extension '{fileExtension}' for '{filePath}' is not supported for PEM certificates.")
 783                }
 784
 785                break;
 786            case "":
 787                // no extension, use the format as the extension
 0788                filePath += fmt == ExportFormat.Pfx ? ".pfx" : ".pem";
 0789                break;
 790            default:
 0791                throw new NotSupportedException(
 0792                    $"File extension '{fileExtension}' for '{filePath}' is not supported. Use .pfx or .pem.");
 793        }
 4794        return filePath;
 795    }
 796
 797    /// <summary>
 798    /// Ensures the output directory exists for the specified file path.
 799    /// </summary>
 800    /// <param name="filePath">The file path to check.</param>
 801    private static void EnsureOutputDirectoryExists(string filePath)
 802    {
 4803        var dir = Path.GetDirectoryName(filePath);
 4804        if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
 805        {
 0806            throw new DirectoryNotFoundException(
 0807                    $"Directory '{dir}' does not exist. Cannot export certificate to {filePath}.");
 808        }
 4809    }
 810
 811    /// <summary>
 812    /// Represents the password shapes used for exporting certificates.
 813    /// </summary>
 4814    private sealed class PasswordShapes(SecureString? secure, char[]? chars) : IDisposable
 815    {
 10816        public SecureString? Secure { get; } = secure;
 14817        public char[]? Chars { get; } = chars;
 818
 819        public void Dispose()
 820        {
 821            try
 822            {
 4823                Secure?.Dispose();
 3824            }
 825            finally
 826            {
 4827                if (Chars is not null)
 828                {
 3829                    Array.Clear(Chars, 0, Chars.Length);
 830                }
 4831            }
 4832        }
 833    }
 834
 835    /// <summary>
 836    /// Creates password shapes from the provided password span.
 837    /// </summary>
 838    /// <param name="password">The password span.</param>
 839    /// <returns>The created password shapes.</returns>
 840    private static PasswordShapes CreatePasswordShapes(ReadOnlySpan<char> password)
 841    {
 4842        var secure = password.IsEmpty ? null : SecureStringUtils.ToSecureString(password);
 4843        var chars = password.IsEmpty ? null : password.ToArray();
 4844        return new PasswordShapes(secure, chars);
 845    }
 846
 847    /// <summary>
 848    /// Exports the specified X509 certificate to a file in the given format.
 849    /// </summary>
 850    /// <param name="cert">The X509Certificate2 to export.</param>
 851    /// <param name="filePath">The file path to export the certificate to.</param>
 852    /// <param name="password">The SecureString password to protect the exported certificate.</param>
 853    private static void ExportPfx(X509Certificate2 cert, string filePath, SecureString? password)
 854    {
 2855        var pfx = cert.Export(X509ContentType.Pfx, password);
 2856        File.WriteAllBytes(filePath, pfx);
 2857    }
 858
 859    /// <summary>
 860    /// Exports the specified X509 certificate to a file in the given format.
 861    /// </summary>
 862    /// <param name="cert">The X509Certificate2 to export.</param>
 863    /// <param name="filePath">The file path to export the certificate to.</param>
 864    /// <param name="password">The SecureString password to protect the exported certificate.</param>
 865    /// <param name="includePrivateKey">Whether to include the private key in the export.</param>
 866    private static void ExportPem(X509Certificate2 cert, string filePath, ReadOnlySpan<char> password, bool includePriva
 867    {
 868        // Write certificate first, then dispose writer before optional key append to avoid file locks on Windows
 2869        using (var sw = new StreamWriter(filePath, false, Encoding.ASCII))
 870        {
 2871            new PemWriter(sw).WriteObject(DotNetUtilities.FromX509Certificate(cert));
 2872        }
 873
 2874        if (includePrivateKey)
 875        {
 1876            WritePrivateKey(cert, password, filePath);
 877            // Fallback safeguard: if append was requested but key block missing, try again
 878            try
 879            {
 1880                if (ShouldAppendKeyToPem && !File.ReadAllText(filePath).Contains("PRIVATE KEY", StringComparison.Ordinal
 881                {
 0882                    var baseName = Path.GetFileNameWithoutExtension(filePath);
 0883                    var dir = Path.GetDirectoryName(filePath);
 0884                    var keyFile = string.IsNullOrEmpty(dir) ? baseName + ".key" : Path.Combine(dir, baseName + ".key");
 0885                    if (File.Exists(keyFile))
 886                    {
 0887                        File.AppendAllText(filePath, Environment.NewLine + File.ReadAllText(keyFile));
 888                    }
 889                }
 1890            }
 0891            catch (Exception ex)
 892            {
 0893                Log.Debug(ex, "Fallback attempt to append private key to PEM failed");
 0894            }
 895        }
 2896    }
 897
 898    /// <summary>
 899    /// Writes the private key of the specified X509 certificate to a file.
 900    /// </summary>
 901    /// <param name="cert">The X509Certificate2 to export.</param>
 902    /// <param name="password">The SecureString password to protect the exported private key.</param>
 903    /// <param name="certFilePath">The file path to export the certificate to.</param>
 904    private static void WritePrivateKey(X509Certificate2 cert, ReadOnlySpan<char> password, string certFilePath)
 905    {
 1906        if (!cert.HasPrivateKey)
 907        {
 0908            throw new InvalidOperationException(
 0909                "Certificate does not contain a private key; cannot export private key PEM.");
 910        }
 911
 912        AsymmetricAlgorithm key;
 913
 914        try
 915        {
 916            // Try RSA first, then ECDSA
 1917            key = (AsymmetricAlgorithm?)cert.GetRSAPrivateKey()
 1918                  ?? cert.GetECDsaPrivateKey()
 1919                  ?? throw new NotSupportedException(
 1920                        "Certificate private key is neither RSA nor ECDSA, or is not accessible.");
 1921        }
 0922        catch (CryptographicException ex) when (ex.HResult == unchecked((int)0x80090016))
 923        {
 924            // 0x80090016 = NTE_BAD_KEYSET  → "Keyset does not exist"
 0925            throw new InvalidOperationException(
 0926                "The certificate reports a private key, but the key container ('keyset') is not accessible. " +
 0927                "This usually means the certificate was loaded without its private key, or the current process " +
 0928                "identity does not have permission to access the key. Re-import the certificate from a PFX " +
 0929                "with the private key and X509KeyStorageFlags.Exportable, or adjust key permissions.",
 0930                ex);
 931        }
 932
 933        byte[] keyDer;
 934        string pemLabel;
 935
 1936        if (password.IsEmpty)
 937        {
 938            // unencrypted PKCS#8
 0939            keyDer = key switch
 0940            {
 0941                RSA rsa => rsa.ExportPkcs8PrivateKey(),
 0942                ECDsa ecc => ecc.ExportPkcs8PrivateKey(),
 0943                _ => throw new NotSupportedException("Only RSA and ECDSA private keys are supported.")
 0944            };
 0945            pemLabel = "PRIVATE KEY";
 946        }
 947        else
 948        {
 949            // encrypted PKCS#8
 1950            var pbe = new PbeParameters(
 1951                PbeEncryptionAlgorithm.Aes256Cbc,
 1952                HashAlgorithmName.SHA256,
 1953                iterationCount: 100_000);
 954
 1955            keyDer = key switch
 1956            {
 1957                RSA rsa => rsa.ExportEncryptedPkcs8PrivateKey(password, pbe),
 0958                ECDsa ecc => ecc.ExportEncryptedPkcs8PrivateKey(password, pbe),
 0959                _ => throw new NotSupportedException("Only RSA and ECDSA private keys are supported.")
 1960            };
 1961            pemLabel = "ENCRYPTED PRIVATE KEY";
 962        }
 963
 1964        var keyPem = PemEncoding.WriteString(pemLabel, keyDer);
 1965        var certDir = Path.GetDirectoryName(certFilePath);
 1966        var baseName = Path.GetFileNameWithoutExtension(certFilePath);
 1967        var keyFilePath = string.IsNullOrEmpty(certDir)
 1968            ? baseName + ".key"
 1969            : Path.Combine(certDir, baseName + ".key");
 970
 1971        File.WriteAllText(keyFilePath, keyPem);
 972
 973        try
 974        {
 1975            if (ShouldAppendKeyToPem)
 976            {
 0977                File.AppendAllText(certFilePath, Environment.NewLine + keyPem);
 978            }
 1979        }
 0980        catch (Exception ex)
 981        {
 0982            Log.Debug(ex,
 0983                "Failed to append private key to certificate PEM file {CertFilePath}; continuing with separate key file 
 0984                certFilePath);
 0985        }
 1986    }
 987
 988    /// <summary>
 989    /// Exports the specified X509 certificate to a file in the given format, using a SecureString password and optional
 990    /// </summary>
 991    /// <param name="cert">The X509Certificate2 to export.</param>
 992    /// <param name="filePath">The file path to export the certificate to.</param>
 993    /// <param name="fmt">The export format (Pfx or Pem).</param>
 994    /// <param name="password">The SecureString password to protect the exported certificate or private key, if applicab
 995    /// <param name="includePrivateKey">Whether to include the private key in the export.</param>
 996    public static void Export(
 997        X509Certificate2 cert,
 998        string filePath,
 999        ExportFormat fmt,
 1000        SecureString password,
 1001        bool includePrivateKey = false)
 1002    {
 11003        if (password is null)
 1004        {
 1005            // Delegate to span-based overload with no password
 01006            Export(cert, filePath, fmt, [], includePrivateKey);
 1007        }
 1008        else
 1009        {
 11010            password.ToSecureSpan(span =>
 11011                Export(cert, filePath, fmt, span, includePrivateKey)
 11012            // this will run your span‐based implementation,
 11013            // then immediately zero & free the unmanaged buffer
 11014            );
 1015        }
 11016    }
 1017
 1018    /// <summary>
 1019    /// Creates a self-signed certificate from the given RSA JWK JSON and exports it
 1020    /// as a PEM certificate (optionally including the private key) to the specified path.
 1021    /// </summary>
 1022    /// <param name="jwkJson">The RSA JWK JSON string.</param>
 1023    /// <param name="filePath">
 1024    /// Target file path. If no extension is provided, ".pem" will be added.
 1025    /// </param>
 1026    /// <param name="password">
 1027    /// Optional password used to encrypt the private key when <paramref name="includePrivateKey"/> is true.
 1028    /// Ignored when <paramref name="includePrivateKey"/> is false.
 1029    /// </param>
 1030    /// <param name="includePrivateKey">
 1031    /// If true, the PEM export will include the private key (and create a .key file as per Export logic).
 1032    /// </param>
 1033    public static void ExportPemFromJwkJson(
 1034        string jwkJson,
 1035        string filePath,
 1036        ReadOnlySpan<char> password = default,
 1037        bool includePrivateKey = false)
 1038    {
 01039        if (string.IsNullOrWhiteSpace(jwkJson))
 1040        {
 01041            throw new ArgumentException("JWK JSON cannot be null or empty.", nameof(jwkJson));
 1042        }
 1043
 1044        // 1) Create a self-signed certificate from the JWK
 01045        var cert = CreateSelfSignedCertificateFromJwk(jwkJson);
 1046
 1047        // 2) Reuse the existing Export pipeline to write PEM (cert + optional key)
 01048        Export(cert, filePath, ExportFormat.Pem, password, includePrivateKey);
 01049    }
 1050
 1051    /// <summary>
 1052    /// Creates a self-signed certificate from the given RSA JWK JSON and exports it
 1053    /// as a PEM certificate (optionally including the private key) to the specified path,
 1054    /// using a <see cref="SecureString"/> password.
 1055    /// </summary>
 1056    /// <param name="jwkJson">The RSA JWK JSON string.</param>
 1057    /// <param name="filePath">Target file path for the PEM output.</param>
 1058    /// <param name="password">
 1059    /// SecureString password used to encrypt the private key when
 1060    /// <paramref name="includePrivateKey"/> is true.
 1061    /// </param>
 1062    /// <param name="includePrivateKey">
 1063    /// If true, the PEM export will include the private key.
 1064    /// </param>
 1065    public static void ExportPemFromJwkJson(
 1066        string jwkJson,
 1067        string filePath,
 1068        SecureString password,
 1069        bool includePrivateKey = false)
 1070    {
 01071        if (password is null)
 1072        {
 1073            // Delegate to span-based overload with no password
 01074            ExportPemFromJwkJson(jwkJson, filePath, [], includePrivateKey);
 01075            return;
 1076        }
 1077
 01078        password.ToSecureSpan(span =>
 01079        {
 01080            ExportPemFromJwkJson(jwkJson, filePath, span, includePrivateKey);
 01081        });
 01082    }
 1083
 1084    #endregion
 1085
 1086    #region JWK
 1087
 11088    private static readonly JsonSerializerOptions s_jwkJsonOptions = new()
 11089    {
 11090        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 11091        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 11092        WriteIndented = false
 11093    };
 1094
 1095    /// <summary>
 1096    /// Creates a self-signed X509 certificate from the provided RSA JWK JSON string.
 1097    /// </summary>
 1098    /// <param name="jwkJson">The JSON string representing the RSA JWK.</param>
 1099    /// <param name="subjectName">The subject name for the certificate.</param>
 1100    /// <returns>A self-signed X509Certificate2 instance.</returns>
 1101    /// <exception cref="ArgumentException">Thrown when the JWK JSON is invalid.</exception>
 1102    /// <exception cref="NotSupportedException"></exception>
 1103    public static X509Certificate2 CreateSelfSignedCertificateFromJwk(
 1104        string jwkJson,
 1105        string subjectName = "CN=client-jwt")
 1106    {
 01107        var jwk = JsonSerializer.Deserialize<RsaJwk>(jwkJson)
 01108                  ?? throw new ArgumentException("Invalid JWK JSON");
 1109
 01110        if (!string.Equals(jwk.Kty, "RSA", StringComparison.OrdinalIgnoreCase))
 1111        {
 01112            throw new NotSupportedException("Only RSA JWKs are supported.");
 1113        }
 1114
 01115        var rsaParams = new RSAParameters
 01116        {
 01117            Modulus = Base64UrlEncoder.DecodeBytes(jwk.N),
 01118            Exponent = Base64UrlEncoder.DecodeBytes(jwk.E),
 01119            D = Base64UrlEncoder.DecodeBytes(jwk.D),
 01120            P = Base64UrlEncoder.DecodeBytes(jwk.P),
 01121            Q = Base64UrlEncoder.DecodeBytes(jwk.Q),
 01122            DP = Base64UrlEncoder.DecodeBytes(jwk.DP),
 01123            DQ = Base64UrlEncoder.DecodeBytes(jwk.DQ),
 01124            InverseQ = Base64UrlEncoder.DecodeBytes(jwk.QI)
 01125        };
 1126
 01127        using var rsa = RSA.Create();
 01128        rsa.ImportParameters(rsaParams);
 1129
 01130        var req = new CertificateRequest(
 01131            subjectName,
 01132            rsa,
 01133            HashAlgorithmName.SHA256,
 01134            RSASignaturePadding.Pkcs1);
 1135
 1136        // Self-signed, 1 year validity (tune as you like)
 01137        var notBefore = DateTimeOffset.UtcNow.AddDays(-1);
 01138        var notAfter = notBefore.AddYears(1);
 1139
 01140        var cert = req.CreateSelfSigned(notBefore, notAfter);
 1141
 1142        // Export with private key, re-import as X509Certificate2
 01143        var pfxBytes = cert.Export(X509ContentType.Pfx);
 1144#if NET9_0_OR_GREATER
 01145        return X509CertificateLoader.LoadPkcs12(
 01146            pfxBytes,
 01147            password: default,
 01148            keyStorageFlags: X509KeyStorageFlags.Exportable,
 01149            loaderLimits: Pkcs12LoaderLimits.Defaults);
 1150#else
 1151        return new X509Certificate2(pfxBytes, (string?)null,
 1152            X509KeyStorageFlags.Exportable);
 1153#endif
 01154    }
 1155
 1156    /// <summary>
 1157    /// Builds a Private Key JWT for client authentication using the specified certificate.
 1158    /// </summary>
 1159    /// <param name="key">The security key (X509SecurityKey or JsonWebKey) to sign the JWT.</param>
 1160    /// <param name="clientId">The client ID (issuer and subject) for the JWT.</param>
 1161    /// <param name="tokenEndpoint">The token endpoint URL (audience) for the JWT.</param>
 1162    /// <returns>The generated Private Key JWT as a string.</returns>
 1163    public static string BuildPrivateKeyJwt(
 1164        SecurityKey key,
 1165        string clientId,
 1166        string tokenEndpoint)
 1167    {
 01168        var now = DateTimeOffset.UtcNow;
 1169
 01170        var creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
 01171        var handler = new JsonWebTokenHandler();
 1172
 01173        var descriptor = new SecurityTokenDescriptor
 01174        {
 01175            Issuer = clientId,
 01176            Audience = tokenEndpoint,
 01177            Subject = new ClaimsIdentity(
 01178            [
 01179                new Claim("sub", clientId),
 01180                new Claim("jti", Guid.NewGuid().ToString("N"))
 01181            ]),
 01182            NotBefore = now.UtcDateTime,
 01183            IssuedAt = now.UtcDateTime,
 01184            Expires = now.AddMinutes(2).UtcDateTime,
 01185            SigningCredentials = creds
 01186        };
 1187
 01188        return handler.CreateToken(descriptor);
 1189    }
 1190
 1191    /// <summary>
 1192    /// Builds a Private Key JWT for client authentication using the specified X509 certificate.
 1193    /// </summary>
 1194    /// <param name="certificate">The X509 certificate containing the private key.</param>
 1195    /// <param name="clientId">The client ID (issuer and subject) for the JWT.</param>
 1196    /// <param name="tokenEndpoint">The token endpoint URL (audience) for the JWT.</param>
 1197    /// <returns>The generated Private Key JWT as a string.</returns>
 1198    public static string BuildPrivateKeyJwt(
 1199        X509Certificate2 certificate,
 1200        string clientId,
 1201        string tokenEndpoint)
 1202    {
 01203        var key = new X509SecurityKey(certificate)
 01204        {
 01205            KeyId = certificate.Thumbprint
 01206        };
 1207
 01208        return BuildPrivateKeyJwt(key, clientId, tokenEndpoint);
 1209    }
 1210
 1211    /// <summary>
 1212    /// Builds a Private Key JWT for client authentication using the specified JWK JSON string.
 1213    /// </summary>
 1214    /// <param name="jwkJson">The JWK JSON string representing the key.</param>
 1215    /// <param name="clientId">The client ID (issuer and subject) for the JWT.</param>
 1216    /// <param name="tokenEndpoint">The token endpoint URL (audience) for the JWT.</param>
 1217    /// <returns>The generated Private Key JWT as a string.</returns>
 1218    public static string BuildPrivateKeyJwtFromJwkJson(
 1219        string jwkJson,
 1220        string clientId,
 1221        string tokenEndpoint)
 1222    {
 01223        var jwk = new JsonWebKey(jwkJson);
 1224        // You can set KeyId here if you want to use kid from the JSON:
 1225        // jwk.KeyId is automatically populated from "kid" if present.
 1226
 01227        return BuildPrivateKeyJwt(jwk, clientId, tokenEndpoint);
 1228    }
 1229
 1230    /// <summary>
 1231    /// Builds a JWK JSON (RSA) representation of the given certificate.
 1232    /// By default only public parameters are included (safe for publishing as JWKS).
 1233    /// Set <paramref name="includePrivateParameters"/> to true if you want a full private JWK
 1234    /// (for local storage only – never publish it).
 1235    /// </summary>
 1236    /// <param name="certificate">The X509 certificate to convert.</param>
 1237    /// <param name="includePrivateParameters">Whether to include private key parameters in the JWK.</param>
 1238    /// <returns>The JWK JSON string.</returns>
 1239    public static string CreateJwkJsonFromCertificate(
 1240       X509Certificate2 certificate,
 1241       bool includePrivateParameters = false)
 1242    {
 01243        var x509Key = new X509SecurityKey(certificate)
 01244        {
 01245            KeyId = certificate.Thumbprint?.ToLowerInvariant()
 01246        };
 1247
 1248        // Convert to a JsonWebKey (n, e, kid, x5c, etc.)
 01249        var jwk = JsonWebKeyConverter.ConvertFromX509SecurityKey(
 01250            x509Key,
 01251            representAsRsaKey: true);
 1252
 01253        if (!includePrivateParameters)
 1254        {
 1255            // Clean public JWK
 01256            jwk.D = null;
 01257            jwk.P = null;
 01258            jwk.Q = null;
 01259            jwk.DP = null;
 01260            jwk.DQ = null;
 01261            jwk.QI = null;
 1262        }
 1263        else
 1264        {
 01265            if (!certificate.HasPrivateKey)
 1266            {
 01267                throw new InvalidOperationException("Certificate has no private key.");
 1268            }
 1269
 01270            using var rsa = certificate.GetRSAPrivateKey()
 01271                ?? throw new NotSupportedException("Certificate does not contain an RSA private key.");
 1272
 01273            var p = rsa.ExportParameters(true);
 1274
 01275            jwk.N = Base64UrlEncoder.Encode(p.Modulus);
 01276            jwk.E = Base64UrlEncoder.Encode(p.Exponent);
 01277            jwk.D = Base64UrlEncoder.Encode(p.D);
 01278            jwk.P = Base64UrlEncoder.Encode(p.P);
 01279            jwk.Q = Base64UrlEncoder.Encode(p.Q);
 01280            jwk.DP = Base64UrlEncoder.Encode(p.DP);
 01281            jwk.DQ = Base64UrlEncoder.Encode(p.DQ);
 01282            jwk.QI = Base64UrlEncoder.Encode(p.InverseQ);
 1283        }
 1284
 01285        return JsonSerializer.Serialize(jwk, s_jwkJsonOptions);
 1286    }
 1287
 1288    /// <summary>
 1289    /// Creates an RSA JWK JSON from a given RSA instance (must contain private key).
 1290    /// </summary>
 1291    /// <param name="rsa">The RSA instance with a private key.</param>
 1292    /// <param name="keyId">Optional key identifier (kid) to set on the JWK.</param>
 1293    /// <returns>JWK JSON string containing public and private parameters.</returns>
 1294    public static string CreateJwkJsonFromRsa(RSA rsa, string? keyId = null)
 1295    {
 01296        ArgumentNullException.ThrowIfNull(rsa);
 1297
 1298        // true => includes private key params (d, p, q, dp, dq, qi)
 01299        var p = rsa.ExportParameters(includePrivateParameters: true);
 1300
 01301        if (p.D is null || p.P is null || p.Q is null ||
 01302            p.DP is null || p.DQ is null || p.InverseQ is null)
 1303        {
 01304            throw new InvalidOperationException("RSA key does not contain private parameters.");
 1305        }
 1306
 01307        var jwk = new RsaJwk
 01308        {
 01309            Kty = "RSA",
 01310            N = Base64UrlEncoder.Encode(p.Modulus),
 01311            E = Base64UrlEncoder.Encode(p.Exponent),
 01312            D = Base64UrlEncoder.Encode(p.D),
 01313            P = Base64UrlEncoder.Encode(p.P),
 01314            Q = Base64UrlEncoder.Encode(p.Q),
 01315            DP = Base64UrlEncoder.Encode(p.DP),
 01316            DQ = Base64UrlEncoder.Encode(p.DQ),
 01317            QI = Base64UrlEncoder.Encode(p.InverseQ),
 01318            Kid = keyId
 01319        };
 1320
 01321        return JsonSerializer.Serialize(jwk, s_jwkJsonOptions);
 1322    }
 1323
 1324    /// <summary>
 1325    /// Creates an RSA JWK JSON from a PKCS#1 or PKCS#8 RSA private key in PEM format.
 1326    /// </summary>
 1327    /// <param name="rsaPrivateKeyPem">
 1328    /// PEM containing an RSA private key (e.g. "-----BEGIN RSA PRIVATE KEY----- ...").
 1329    /// </param>
 1330    /// <param name="keyId">Optional key identifier (kid) to set on the JWK.</param>
 1331    /// <returns>JWK JSON string containing public and private parameters.</returns>
 1332    public static string CreateJwkJsonFromRsaPrivateKeyPem(
 1333        string rsaPrivateKeyPem,
 1334        string? keyId = null)
 1335    {
 01336        if (string.IsNullOrWhiteSpace(rsaPrivateKeyPem))
 1337        {
 01338            throw new ArgumentException("RSA private key PEM cannot be null or empty.", nameof(rsaPrivateKeyPem));
 1339        }
 1340
 01341        using var rsa = RSA.Create();
 01342        rsa.ImportFromPem(rsaPrivateKeyPem.AsSpan());
 1343
 01344        return CreateJwkJsonFromRsa(rsa, keyId);
 01345    }
 1346
 1347    #endregion
 1348
 1349    #region  Validation helpers (Test-PodeCertificate equivalent)
 1350    /// <summary>
 1351    /// Validates the specified X509 certificate according to the provided options.
 1352    /// </summary>
 1353    /// <param name="cert">The X509Certificate2 to validate.</param>
 1354    /// <param name="checkRevocation">Whether to check certificate revocation status.</param>
 1355    /// <param name="allowWeakAlgorithms">Whether to allow weak algorithms such as SHA-1 or small key sizes.</param>
 1356    /// <param name="denySelfSigned">Whether to deny self-signed certificates.</param>
 1357    /// <param name="expectedPurpose">A collection of expected key purposes (EKU) for the certificate.</param>
 1358    /// <param name="strictPurpose">If true, the certificate must match the expected purposes exactly.</param>
 1359    /// <returns>True if the certificate is valid according to the specified options; otherwise, false.</returns>
 1360    public static bool Validate(
 1361     X509Certificate2 cert,
 1362     bool checkRevocation = false,
 1363     bool allowWeakAlgorithms = false,
 1364     bool denySelfSigned = false,
 1365     OidCollection? expectedPurpose = null,
 1366     bool strictPurpose = false)
 1367    {
 71368        return Validate(
 71369            cert,
 71370            checkRevocation,
 71371            allowWeakAlgorithms,
 71372            denySelfSigned,
 71373            expectedPurpose,
 71374            strictPurpose,
 71375            certificateChain: null,
 71376            out _);
 1377    }
 1378
 1379    /// <summary>
 1380    /// Validates the specified X509 certificate according to the provided options.
 1381    /// </summary>
 1382    /// <param name="cert">The X509Certificate2 to validate.</param>
 1383    /// <param name="checkRevocation">Whether to check certificate revocation status.</param>
 1384    /// <param name="allowWeakAlgorithms">Whether to allow weak algorithms such as SHA-1 or small key sizes.</param>
 1385    /// <param name="denySelfSigned">Whether to deny self-signed certificates.</param>
 1386    /// <param name="expectedPurpose">A collection of expected key purposes (EKU) for the certificate.</param>
 1387    /// <param name="strictPurpose">If true, the certificate must match the expected purposes exactly.</param>
 1388    /// <param name="certificateChain">Optional chain certificates used to help build trust, such as a private developme
 1389    /// <returns>True if the certificate is valid according to the specified options; otherwise, false.</returns>
 1390    public static bool Validate(
 1391     X509Certificate2 cert,
 1392     bool checkRevocation,
 1393     bool allowWeakAlgorithms,
 1394     bool denySelfSigned,
 1395     OidCollection? expectedPurpose,
 1396     bool strictPurpose,
 1397     X509Certificate2Collection? certificateChain)
 1398    {
 01399        return Validate(
 01400            cert,
 01401            checkRevocation,
 01402            allowWeakAlgorithms,
 01403            denySelfSigned,
 01404            expectedPurpose,
 01405            strictPurpose,
 01406            certificateChain,
 01407            out _);
 1408    }
 1409
 1410    /// <summary>
 1411    /// Validates the specified X509 certificate according to the provided options.
 1412    /// </summary>
 1413    /// <param name="cert">The X509Certificate2 to validate.</param>
 1414    /// <param name="checkRevocation">Whether to check certificate revocation status.</param>
 1415    /// <param name="allowWeakAlgorithms">Whether to allow weak algorithms such as SHA-1 or small key sizes.</param>
 1416    /// <param name="denySelfSigned">Whether to deny self-signed certificates.</param>
 1417    /// <param name="expectedPurpose">A collection of expected key purposes (EKU) for the certificate.</param>
 1418    /// <param name="strictPurpose">If true, the certificate must match the expected purposes exactly.</param>
 1419    /// <param name="failureReason">The reason validation failed; empty when validation succeeds.</param>
 1420    /// <returns>True if the certificate is valid according to the specified options; otherwise, false.</returns>
 1421    public static bool Validate(
 1422     X509Certificate2 cert,
 1423     bool checkRevocation,
 1424     bool allowWeakAlgorithms,
 1425     bool denySelfSigned,
 1426     OidCollection? expectedPurpose,
 1427     bool strictPurpose,
 1428     out string failureReason)
 1429    {
 01430        return Validate(
 01431            cert,
 01432            checkRevocation,
 01433            allowWeakAlgorithms,
 01434            denySelfSigned,
 01435            expectedPurpose,
 01436            strictPurpose,
 01437            certificateChain: null,
 01438            out failureReason);
 1439    }
 1440
 1441    /// <summary>
 1442    /// Validates the specified X509 certificate according to the provided options.
 1443    /// </summary>
 1444    /// <param name="cert">The X509Certificate2 to validate.</param>
 1445    /// <param name="checkRevocation">Whether to check certificate revocation status.</param>
 1446    /// <param name="allowWeakAlgorithms">Whether to allow weak algorithms such as SHA-1 or small key sizes.</param>
 1447    /// <param name="denySelfSigned">Whether to deny self-signed certificates.</param>
 1448    /// <param name="expectedPurpose">A collection of expected key purposes (EKU) for the certificate.</param>
 1449    /// <param name="strictPurpose">If true, the certificate must match the expected purposes exactly.</param>
 1450    /// <param name="certificateChain">Optional chain certificates used to help build trust, such as a private developme
 1451    /// <param name="failureReason">The reason validation failed; empty when validation succeeds.</param>
 1452    /// <returns>True if the certificate is valid according to the specified options; otherwise, false.</returns>
 1453    public static bool Validate(
 1454     X509Certificate2 cert,
 1455     bool checkRevocation,
 1456     bool allowWeakAlgorithms,
 1457     bool denySelfSigned,
 1458     OidCollection? expectedPurpose,
 1459     bool strictPurpose,
 1460     X509Certificate2Collection? certificateChain,
 1461     out string failureReason)
 1462    {
 91463        failureReason = string.Empty;
 1464
 91465        Log.Debug(
 91466            "Validating certificate {Certificate}. CheckRevocation={CheckRevocation}, AllowWeakAlgorithms={AllowWeakAlgo
 91467            DescribeCertificate(cert),
 91468            checkRevocation,
 91469            allowWeakAlgorithms,
 91470            denySelfSigned,
 91471            strictPurpose,
 91472            DescribeExpectedPurposes(expectedPurpose),
 91473            certificateChain?.Count ?? 0);
 1474
 1475        // 1) Validity period
 91476        if (!IsWithinValidityPeriod(cert))
 1477        {
 01478            failureReason = "Certificate is outside validity period.";
 01479            Log.Warning(
 01480                "Certificate validation failed for {Certificate}: {FailureReason} NotBeforeUtc={NotBeforeUtc:O}, NotAfte
 01481                DescribeCertificate(cert),
 01482                failureReason,
 01483                cert.NotBefore.ToUniversalTime(),
 01484                cert.NotAfter.ToUniversalTime(),
 01485                DateTime.UtcNow);
 01486            return false;
 1487        }
 1488
 1489        // 2) Self-signed policy
 91490        var isSelfSigned = cert.Subject == cert.Issuer;
 91491        if (denySelfSigned && isSelfSigned)
 1492        {
 11493            failureReason = "Self-signed certificates are denied by policy.";
 11494            Log.Warning(
 11495                "Certificate validation failed for {Certificate}: {FailureReason}",
 11496                DescribeCertificate(cert),
 11497                failureReason);
 11498            return false;
 1499        }
 1500
 1501        // Pre-compute weakness so we can apply it consistently across validation steps.
 81502        var isWeak = UsesWeakAlgorithms(cert);
 1503
 1504        // 3) Chain build (with optional revocation)
 81505        if (!BuildChainOk(cert, checkRevocation, isSelfSigned, allowWeakAlgorithms, isWeak, certificateChain, out var ch
 1506        {
 11507            failureReason = string.IsNullOrWhiteSpace(chainFailureReason)
 11508                ? "Certificate chain validation failed."
 11509                : chainFailureReason;
 11510            Log.Warning(
 11511                "Certificate validation failed for {Certificate}: {FailureReason} CheckRevocation={CheckRevocation}, IsS
 11512                DescribeCertificate(cert),
 11513                failureReason,
 11514                checkRevocation,
 11515                isSelfSigned,
 11516                allowWeakAlgorithms,
 11517                isWeak,
 11518                certificateChain?.Count ?? 0);
 11519            return false;
 1520        }
 1521
 1522        // 4) EKU / purposes
 71523        if (!PurposesOk(cert, expectedPurpose, strictPurpose))
 1524        {
 11525            failureReason = "EKU purpose mismatch.";
 11526            Log.Warning(
 11527                "Certificate validation failed for {Certificate}: {FailureReason} Expected={ExpectedPurposes}, Actual={A
 11528                DescribeCertificate(cert),
 11529                failureReason,
 11530                DescribeExpectedPurposes(expectedPurpose),
 21531                string.Join(",", GetEkuOids(cert).OrderBy(static value => value, StringComparer.Ordinal)),
 11532                strictPurpose);
 11533            return false;
 1534        }
 1535
 1536        // 5) Weak algorithms
 61537        if (!allowWeakAlgorithms && isWeak)
 1538        {
 11539            failureReason = string.Join("; ", GetWeakAlgorithmFindings(cert));
 11540            if (string.IsNullOrWhiteSpace(failureReason))
 1541            {
 01542                failureReason = "Weak algorithm policy violation.";
 1543            }
 11544            Log.Warning(
 11545                "Certificate validation failed for {Certificate}: {FailureReason}",
 11546                DescribeCertificate(cert),
 11547                failureReason);
 11548            return false;
 1549        }
 1550
 51551        Log.Debug("Certificate validation succeeded for {Certificate}", DescribeCertificate(cert));
 51552        return true;   // ✅ everything passed
 1553    }
 1554
 1555    /// <summary>
 1556    /// Checks if the certificate is within its validity period.
 1557    /// </summary>
 1558    /// <param name="cert">The X509Certificate2 to check.</param>
 1559    /// <returns>True if the certificate is within its validity period; otherwise, false.</returns>
 1560    private static bool IsWithinValidityPeriod(X509Certificate2 cert)
 1561    {
 91562        var notBeforeUtc = cert.NotBefore.Kind == DateTimeKind.Utc
 91563            ? cert.NotBefore
 91564            : cert.NotBefore.ToUniversalTime();
 1565
 91566        var notAfterUtc = cert.NotAfter.Kind == DateTimeKind.Utc
 91567            ? cert.NotAfter
 91568            : cert.NotAfter.ToUniversalTime();
 1569
 91570        var nowUtc = DateTime.UtcNow;
 91571        return nowUtc >= notBeforeUtc && nowUtc <= notAfterUtc;
 1572    }
 1573
 1574    /// <summary>
 1575    /// Checks if the certificate chain is valid.
 1576    /// </summary>
 1577    /// <param name="cert">The X509Certificate2 to check.</param>
 1578    /// <param name="checkRevocation">Whether to check certificate revocation status.</param>
 1579    /// <param name="isSelfSigned">Whether the certificate is self-signed.</param>
 1580    /// <param name="certificateChain">Optional chain certificates used to help build trust.</param>
 1581    /// <param name="failureReason">The detailed failure reason when validation fails.</param>
 1582    /// <returns>True if the certificate chain is valid; otherwise, false.</returns>
 1583    private static bool BuildChainOk(X509Certificate2 cert, bool checkRevocation, bool isSelfSigned, X509Certificate2Col
 1584    {
 71585        failureReason = string.Empty;
 71586        using var chain = CreateValidationChain(checkRevocation, cert, isSelfSigned, certificateChain);
 1587
 71588        var ok = chain.Build(cert);
 71589        if (ok)
 1590        {
 61591            Log.Debug("Certificate chain validation succeeded for {Certificate}", DescribeCertificate(cert));
 61592            return true;
 1593        }
 1594
 11595        var statusSummary = DescribeChainStatuses(chain.ChainStatus);
 1596
 11597        if (!isSelfSigned)
 1598        {
 11599            failureReason = BuildChainFailureReason(statusSummary);
 11600            Log.Warning(
 11601                "Certificate chain validation failed for {Certificate}. ChainStatuses={ChainStatuses}, SuppliedChainCoun
 11602                DescribeCertificate(cert),
 11603                statusSummary,
 11604                certificateChain?.Count ?? 0);
 11605            return false;
 1606        }
 1607
 1608        // Some platforms still report non-fatal statuses for self-signed roots.
 1609        // Treat these as acceptable for self-signed certificates.
 01610        var allowed = X509ChainStatusFlags.UntrustedRoot | X509ChainStatusFlags.PartialChain;
 01611        if (!checkRevocation)
 1612        {
 01613            allowed |= X509ChainStatusFlags.RevocationStatusUnknown | X509ChainStatusFlags.OfflineRevocation;
 1614        }
 1615
 01616        var combined = X509ChainStatusFlags.NoError;
 01617        foreach (var status in chain.ChainStatus)
 1618        {
 01619            combined |= status.Status;
 1620        }
 1621
 01622        var unexpected = combined & ~allowed;
 01623        if (unexpected == 0)
 1624        {
 01625            Log.Debug(
 01626                "Self-signed chain validation accepted non-fatal statuses for {Certificate}. ChainStatuses={ChainStatuse
 01627                DescribeCertificate(cert),
 01628                statusSummary,
 01629                allowed);
 01630            return true;
 1631        }
 1632
 01633        Log.Warning(
 01634            "Self-signed chain validation failed for {Certificate}. ChainStatuses={ChainStatuses}, AllowedFlags={Allowed
 01635            DescribeCertificate(cert),
 01636            statusSummary,
 01637            allowed,
 01638            unexpected);
 01639        failureReason = BuildChainFailureReason(statusSummary, selfSigned: true);
 01640        return false;
 71641    }
 1642
 1643    /// <summary>
 1644    /// Checks if the certificate chain is valid.
 1645    /// </summary>
 1646    /// <param name="cert">The X509Certificate2 to check.</param>
 1647    /// <param name="checkRevocation">Whether to check certificate revocation status.</param>
 1648    /// <param name="isSelfSigned">Whether the certificate is self-signed.</param>
 1649    /// <param name="allowWeakAlgorithms">Whether weak algorithms are allowed.</param>
 1650    /// <param name="isWeak">Whether the certificate is considered weak by this library.</param>
 1651    /// <param name="certificateChain">Optional chain certificates used to help build trust.</param>
 1652    /// <param name="failureReason">The detailed failure reason when validation fails.</param>
 1653    /// <returns>True if the certificate chain is valid; otherwise, false.</returns>
 1654    private static bool BuildChainOk(
 1655        X509Certificate2 cert,
 1656        bool checkRevocation,
 1657        bool isSelfSigned,
 1658        bool allowWeakAlgorithms,
 1659        bool isWeak,
 1660        X509Certificate2Collection? certificateChain,
 1661        out string failureReason)
 1662    {
 81663        failureReason = string.Empty;
 1664
 81665        if (isSelfSigned && allowWeakAlgorithms && isWeak)
 1666        {
 11667            Log.Warning(
 11668                "Bypassing certificate chain validation for {Certificate}: self-signed weak certificate is allowed by po
 11669                DescribeCertificate(cert));
 11670            return true;
 1671        }
 1672
 71673        return BuildChainOk(cert, checkRevocation, isSelfSigned, certificateChain, out failureReason);
 1674    }
 1675
 1676    /// <summary>
 1677    /// Creates an X509 chain configured for certificate validation.
 1678    /// </summary>
 1679    /// <param name="checkRevocation">Whether revocation should be checked online.</param>
 1680    /// <param name="cert">The target certificate being validated.</param>
 1681    /// <param name="isSelfSigned">Whether the target certificate is self-signed.</param>
 1682    /// <param name="certificateChain">Optional chain certificates used to help build trust.</param>
 1683    /// <returns>A configured chain instance.</returns>
 1684    private static X509Chain CreateValidationChain(bool checkRevocation, X509Certificate2 cert, bool isSelfSigned, X509C
 1685    {
 71686        var chain = new X509Chain();
 71687        chain.ChainPolicy.RevocationMode = checkRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck;
 71688        chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EndCertificateOnly;
 71689        chain.ChainPolicy.DisableCertificateDownloads = !checkRevocation;
 1690
 71691        ConfigureValidationChainPolicy(chain, cert, isSelfSigned, certificateChain);
 71692        return chain;
 1693    }
 1694
 1695    /// <summary>
 1696    /// Applies custom trust and extra-store certificates to the validation chain.
 1697    /// </summary>
 1698    /// <param name="chain">The chain to configure.</param>
 1699    /// <param name="cert">The target certificate being validated.</param>
 1700    /// <param name="isSelfSigned">Whether the target certificate is self-signed.</param>
 1701    /// <param name="certificateChain">Optional chain certificates used to help build trust.</param>
 1702    private static void ConfigureValidationChainPolicy(X509Chain chain, X509Certificate2 cert, bool isSelfSigned, X509Ce
 1703    {
 71704        if (isSelfSigned)
 1705        {
 51706            chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
 51707            _ = chain.ChainPolicy.CustomTrustStore.Add(cert);
 51708            chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
 51709            return;
 1710        }
 1711
 21712        if (certificateChain is not { Count: > 0 })
 1713        {
 11714            return;
 1715        }
 1716
 11717        var intermediates = GetIntermediateCertificates(certificateChain, cert);
 11718        if (intermediates.Count > 0)
 1719        {
 01720            chain.ChainPolicy.ExtraStore.AddRange(intermediates);
 1721        }
 1722
 11723        var roots = GetRootCertificates(certificateChain, cert);
 11724        if (roots.Count > 0)
 1725        {
 11726            chain.ChainPolicy.CustomTrustStore.AddRange(roots);
 11727            chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
 1728        }
 11729    }
 1730
 1731    /// <summary>
 1732    /// Gets supplied self-signed root certificates that can anchor custom trust.
 1733    /// </summary>
 1734    /// <param name="certificateChain">The supplied chain certificates.</param>
 1735    /// <param name="cert">The target certificate being validated.</param>
 1736    /// <returns>A collection of self-signed root certificates.</returns>
 1737    private static X509Certificate2Collection GetRootCertificates(X509Certificate2Collection certificateChain, X509Certi
 1738    {
 11739        var roots = new X509Certificate2Collection();
 41740        foreach (var chainCertificate in certificateChain)
 1741        {
 11742            if (!CertificateMatches(chainCertificate, cert) && IsSelfSignedCertificate(chainCertificate))
 1743            {
 11744                _ = roots.Add(chainCertificate);
 1745            }
 1746        }
 1747
 11748        return roots;
 1749    }
 1750
 1751    /// <summary>
 1752    /// Gets supplied intermediate certificates that should be added to the chain extra store.
 1753    /// </summary>
 1754    /// <param name="certificateChain">The supplied chain certificates.</param>
 1755    /// <param name="cert">The target certificate being validated.</param>
 1756    /// <returns>A collection of non-self-signed auxiliary certificates.</returns>
 1757    private static X509Certificate2Collection GetIntermediateCertificates(X509Certificate2Collection certificateChain, X
 1758    {
 11759        var intermediates = new X509Certificate2Collection();
 41760        foreach (var chainCertificate in certificateChain)
 1761        {
 11762            if (!CertificateMatches(chainCertificate, cert) && !IsSelfSignedCertificate(chainCertificate))
 1763            {
 01764                _ = intermediates.Add(chainCertificate);
 1765            }
 1766        }
 1767
 11768        return intermediates;
 1769    }
 1770
 1771    /// <summary>
 1772    /// Determines whether two certificates represent the same certificate material.
 1773    /// </summary>
 1774    /// <param name="left">The first certificate.</param>
 1775    /// <param name="right">The second certificate.</param>
 1776    /// <returns><c>true</c> when the certificates have the same thumbprint; otherwise, <c>false</c>.</returns>
 1777    private static bool CertificateMatches(X509Certificate2 left, X509Certificate2 right) =>
 21778        string.Equals(left.Thumbprint, right.Thumbprint, StringComparison.OrdinalIgnoreCase);
 1779
 1780    /// <summary>
 1781    /// Determines whether the certificate is self-signed.
 1782    /// </summary>
 1783    /// <param name="cert">The certificate to inspect.</param>
 1784    /// <returns><c>true</c> when the subject and issuer are the same; otherwise, <c>false</c>.</returns>
 21785    private static bool IsSelfSignedCertificate(X509Certificate2 cert) => cert.Subject == cert.Issuer;
 1786
 1787    /// <summary>
 1788    /// Builds a readable failure reason for chain validation failures.
 1789    /// </summary>
 1790    /// <param name="statusSummary">The formatted chain status summary.</param>
 1791    /// <param name="selfSigned">Whether the failure came from self-signed validation.</param>
 1792    /// <returns>A user-readable failure reason.</returns>
 1793    private static string BuildChainFailureReason(string statusSummary, bool selfSigned = false)
 1794    {
 11795        var prefix = selfSigned
 11796            ? "Self-signed certificate chain validation failed"
 11797            : "Certificate chain validation failed";
 1798
 11799        return string.IsNullOrWhiteSpace(statusSummary)
 11800            ? prefix + "."
 11801            : $"{prefix}: {statusSummary}";
 1802    }
 1803
 1804    /// <summary>
 1805    /// Checks if the certificate has the expected key purposes (EKU).
 1806    /// </summary>
 1807    /// <param name="cert">The X509Certificate2 to check.</param>
 1808    /// <param name="expectedPurpose">A collection of expected key purposes (EKU) for the certificate.</param>
 1809    /// <param name="strictPurpose">If true, the certificate must match the expected purposes exactly.</param>
 1810    /// <returns>True if the certificate has the expected purposes; otherwise, false.</returns>
 1811    private static bool PurposesOk(X509Certificate2 cert, OidCollection? expectedPurpose, bool strictPurpose)
 1812    {
 71813        if (expectedPurpose is not { Count: > 0 })
 1814        {
 41815            return true; // nothing to check
 1816        }
 1817
 31818        var eku = GetEkuOids(cert);
 31819        var wanted = NormalizeExpectedPurposeOids(expectedPurpose);
 1820
 31821        return wanted.Count == 0 || (eku.Count != 0 && (strictPurpose ? eku.SetEquals(wanted) : wanted.IsSubsetOf(eku)))
 1822    }
 1823
 1824    /// <summary>
 1825    /// Normalizes expected EKU OIDs into a canonical set.
 1826    /// </summary>
 1827    /// <param name="expectedPurpose">The expected purpose collection.</param>
 1828    /// <returns>A normalized set of OID values.</returns>
 71829    private static HashSet<string> NormalizeExpectedPurposeOids(OidCollection expectedPurpose) => expectedPurpose
 71830        .Cast<Oid>()
 91831        .Select(static o => o.Value)
 91832        .Where(static v => !string.IsNullOrWhiteSpace(v))
 91833        .Select(static v => v!)
 71834        .ToHashSet(StringComparer.Ordinal);
 1835
 1836    /// <summary>
 1837    /// Extracts EKU OIDs from the certificate, robustly across platforms.
 1838    /// </summary>
 1839    /// <param name="cert">The certificate to inspect.</param>
 1840    /// <returns>A set of EKU OID strings.</returns>
 1841    private static HashSet<string> GetEkuOids(X509Certificate2 cert)
 1842    {
 41843        var set = new HashSet<string>(StringComparer.Ordinal);
 1844
 1845        // EKU extension OID
 41846        var ext = cert.Extensions["2.5.29.37"];
 41847        if (ext == null)
 1848        {
 01849            return set;
 1850        }
 1851
 41852        var ekuExt = ext as X509EnhancedKeyUsageExtension
 41853            ?? new X509EnhancedKeyUsageExtension(ext, ext.Critical);
 1854
 241855        foreach (var oid in ekuExt.EnhancedKeyUsages.Cast<Oid>())
 1856        {
 81857            if (!string.IsNullOrWhiteSpace(oid.Value))
 1858            {
 81859                _ = set.Add(oid.Value);
 1860            }
 1861        }
 1862
 41863        return set;
 1864    }
 1865
 1866    /// <summary>
 1867    /// Checks if the certificate uses weak algorithms.
 1868    /// </summary>
 1869    /// <param name="cert">The X509Certificate2 to check.</param>
 1870    /// <returns>True if the certificate uses weak algorithms; otherwise, false.</returns>
 81871    private static bool UsesWeakAlgorithms(X509Certificate2 cert) => GetWeakAlgorithmFindings(cert).Count != 0;
 1872
 1873    /// <summary>
 1874    /// Computes weak algorithm findings for the certificate.
 1875    /// </summary>
 1876    /// <param name="cert">The certificate to inspect.</param>
 1877    /// <returns>A list of weak-algorithm findings; empty when the certificate is considered strong.</returns>
 1878    private static List<string> GetWeakAlgorithmFindings(X509Certificate2 cert)
 1879    {
 91880        var findings = new List<string>();
 1881
 91882        var isSha1 = cert.SignatureAlgorithm?.FriendlyName?
 91883                           .Contains("sha1", StringComparison.OrdinalIgnoreCase) == true;
 91884        if (isSha1)
 1885        {
 01886            findings.Add($"Signature algorithm '{cert.SignatureAlgorithm?.FriendlyName}'");
 1887        }
 1888
 91889        var rsa = cert.GetRSAPublicKey();
 91890        if (rsa is { KeySize: < 2048 })
 1891        {
 31892            findings.Add($"RSA key size {rsa.KeySize} < 2048");
 1893        }
 1894
 91895        var dsa = cert.GetDSAPublicKey();
 91896        if (dsa is { KeySize: < 2048 })
 1897        {
 01898            findings.Add($"DSA key size {dsa.KeySize} < 2048");
 1899        }
 1900
 91901        var ecdsa = cert.GetECDsaPublicKey();
 91902        if (ecdsa is { KeySize: < 256 })
 1903        {
 01904            findings.Add($"ECDSA key size {ecdsa.KeySize} < 256");
 1905        }
 1906
 91907        return findings;
 1908    }
 1909
 1910    /// <summary>
 1911    /// Creates a compact string describing the certificate identity for logs.
 1912    /// </summary>
 1913    /// <param name="cert">The certificate to describe.</param>
 1914    /// <returns>A concise description suitable for structured log output.</returns>
 1915    private static string DescribeCertificate(X509Certificate2 cert) =>
 261916        $"Subject='{cert.Subject}', Issuer='{cert.Issuer}', Thumbprint='{cert.Thumbprint}', Serial='{cert.SerialNumber}'
 1917
 1918    /// <summary>
 1919    /// Formats expected EKU purposes for log output.
 1920    /// </summary>
 1921    /// <param name="expectedPurpose">Expected EKU purposes.</param>
 1922    /// <returns>A comma-separated OID list or a sentinel value when none were specified.</returns>
 1923    private static string DescribeExpectedPurposes(OidCollection? expectedPurpose)
 1924    {
 101925        if (expectedPurpose is not { Count: > 0 })
 1926        {
 61927            return "<none>";
 1928        }
 1929
 91930        var values = NormalizeExpectedPurposeOids(expectedPurpose).OrderBy(static value => value, StringComparer.Ordinal
 41931        return string.Join(",", values);
 1932    }
 1933
 1934    /// <summary>
 1935    /// Formats chain statuses for diagnostics.
 1936    /// </summary>
 1937    /// <param name="statuses">The chain statuses to format.</param>
 1938    /// <returns>A compact status summary.</returns>
 1939    private static string DescribeChainStatuses(X509ChainStatus[] statuses)
 1940    {
 11941        return statuses.Length == 0
 11942            ? "<none>"
 11943            : string.Join(
 11944            " | ",
 11945            statuses.Select(static status =>
 21946                $"{status.Status}: {status.StatusInformation?.Trim()}"));
 1947    }
 1948
 1949    /// <summary>
 1950    /// Resolves issuer material for certificates signed by an existing CA.
 1951    /// </summary>
 1952    /// <param name="issuerCertificate">The issuer certificate containing the signing private key.</param>
 1953    /// <returns>The issuer subject name, private key, and public key info, or null for self-signed certificates.</retur
 1954    /// <exception cref="InvalidOperationException">Thrown when the issuer certificate cannot sign child certificates.</
 1955    private static IssuerMaterial? ResolveIssuerMaterial(X509Certificate2? issuerCertificate)
 1956    {
 281957        if (issuerCertificate is null)
 1958        {
 231959            return null;
 1960        }
 1961
 51962        ValidateIssuerCertificate(issuerCertificate);
 1963
 41964        var bcCertificate = DotNetUtilities.FromX509Certificate(issuerCertificate);
 41965        return new IssuerMaterial(
 41966            bcCertificate.SubjectDN,
 41967            GetPrivateKeyParameter(issuerCertificate),
 41968            SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(bcCertificate.GetPublicKey()));
 1969    }
 1970
 1971    /// <summary>
 1972    /// Validates that the supplied issuer certificate can sign child certificates.
 1973    /// </summary>
 1974    /// <param name="issuerCertificate">The issuer certificate to validate.</param>
 1975    /// <exception cref="InvalidOperationException">Thrown when the issuer certificate is not a CA or lacks signing mate
 1976    private static void ValidateIssuerCertificate(X509Certificate2 issuerCertificate)
 1977    {
 51978        if (!issuerCertificate.HasPrivateKey)
 1979        {
 01980            throw new InvalidOperationException("Issuer certificate must contain a private key.");
 1981        }
 1982
 51983        var basicConstraints = issuerCertificate.Extensions.OfType<X509BasicConstraintsExtension>().SingleOrDefault();
 51984        if (basicConstraints is null || !basicConstraints.CertificateAuthority)
 1985        {
 11986            throw new InvalidOperationException("Issuer certificate must be a CA certificate.");
 1987        }
 1988
 41989        var keyUsage = issuerCertificate.Extensions.OfType<X509KeyUsageExtension>().SingleOrDefault();
 41990        if (keyUsage is not null && !keyUsage.KeyUsages.HasFlag(X509KeyUsageFlags.KeyCertSign))
 1991        {
 01992            throw new InvalidOperationException("Issuer certificate must allow certificate signing (KeyCertSign).");
 1993        }
 41994    }
 1995
 1996    /// <summary>
 1997    /// Converts a .NET certificate private key into a Bouncy Castle private key parameter.
 1998    /// </summary>
 1999    /// <param name="certificate">The certificate containing the private key.</param>
 2000    /// <returns>The Bouncy Castle private key parameter.</returns>
 2001    /// <exception cref="InvalidOperationException">Thrown when the certificate does not contain a supported private key
 2002    private static AsymmetricKeyParameter GetPrivateKeyParameter(X509Certificate2 certificate)
 2003    {
 2004        try
 2005        {
 42006            if (certificate.GetRSAPrivateKey() is RSA rsa)
 2007            {
 42008                return GetPrivateKeyParameter(rsa);
 2009            }
 2010
 02011            if (certificate.GetECDsaPrivateKey() is ECDsa ecdsa)
 2012            {
 02013                return GetPrivateKeyParameter(ecdsa);
 2014            }
 02015        }
 02016        catch (CryptographicException ex)
 2017        {
 02018            throw new InvalidOperationException(
 02019                "Issuer certificate private key must support PKCS#8 export. Create or import the issuer certificate with
 02020                ex);
 2021        }
 2022
 02023        throw new InvalidOperationException("Only RSA and ECDSA issuer certificates are supported.");
 42024    }
 2025
 2026    /// <summary>
 2027    /// Converts an RSA private key into a Bouncy Castle private key parameter.
 2028    /// </summary>
 2029    /// <param name="rsa">The RSA private key.</param>
 2030    /// <returns>The Bouncy Castle private key parameter.</returns>
 2031    private static AsymmetricKeyParameter GetPrivateKeyParameter(RSA rsa)
 2032    {
 2033        try
 2034        {
 42035            return PrivateKeyFactory.CreateKey(rsa.ExportPkcs8PrivateKey());
 2036        }
 02037        catch (CryptographicException)
 2038        {
 02039            return DotNetUtilities.GetRsaKeyPair(rsa).Private;
 2040        }
 42041    }
 2042
 2043    /// <summary>
 2044    /// Converts an ECDSA private key into a Bouncy Castle private key parameter.
 2045    /// </summary>
 2046    /// <param name="ecdsa">The ECDSA private key.</param>
 2047    /// <returns>The Bouncy Castle private key parameter.</returns>
 2048    private static AsymmetricKeyParameter GetPrivateKeyParameter(ECDsa ecdsa)
 2049    {
 2050        try
 2051        {
 02052            return PrivateKeyFactory.CreateKey(ecdsa.ExportPkcs8PrivateKey());
 2053        }
 02054        catch (CryptographicException)
 2055        {
 02056            return DotNetUtilities.GetECDsaKeyPair(ecdsa).Private;
 2057        }
 02058    }
 2059
 2060    /// <summary>
 2061    /// Creates the development root certificate when the caller does not provide one.
 2062    /// </summary>
 2063    /// <param name="options">The development certificate creation options.</param>
 2064    /// <returns>A newly created CA root certificate.</returns>
 2065    private static X509Certificate2 CreateDevelopmentRoot(SelfSignedOptions options) =>
 22066        CreateSelfSignedCertificate(new SelfSignedOptions(
 22067            [options.RootName],
 22068            KeyType: options.KeyType,
 22069            KeyLength: options.KeyLength,
 22070            ValidDays: options.RootValidDays,
 22071            Ephemeral: options.Ephemeral,
 22072            Exportable: options.Exportable,
 22073            IsCertificateAuthority: true));
 2074
 2075    /// <summary>
 2076    /// Adds the development root certificate to the Windows CurrentUser Root store when requested.
 2077    /// </summary>
 2078    /// <param name="options">The development certificate creation options.</param>
 2079    /// <param name="certificate">The effective development root certificate.</param>
 2080    /// <returns><c>true</c> when the certificate is present in the Windows CurrentUser Root store after the operation; 
 2081    /// <exception cref="InvalidOperationException">Thrown when root trust is requested on a non-Windows platform.</exce
 2082    private static bool TrustDevelopmentRootIfRequested(SelfSignedOptions options, X509Certificate2 certificate)
 2083    {
 32084        if (!options.TrustRoot)
 2085        {
 32086            return false;
 2087        }
 2088
 02089        if (!OperatingSystem.IsWindows())
 2090        {
 02091            throw new InvalidOperationException("TrustRoot is currently supported only on Windows because it uses the Wi
 2092        }
 2093
 02094        using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
 02095        store.Open(OpenFlags.ReadWrite);
 2096
 02097        var existing = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, false);
 02098        if (existing.Count == 0)
 2099        {
 2100            // Import the certificate without private key to avoid accidentally exposing the private key in the Root sto
 02101            using var publicOnlyCertificate = CreatePublicOnlyCertificate(certificate);
 02102            store.Add(publicOnlyCertificate);
 2103        }
 2104
 02105        return true;
 02106    }
 2107
 2108    /// <summary>
 2109    /// Creates a public-only copy of a certificate without carrying over the private key.
 2110    /// </summary>
 2111    /// <param name="certificate">The source certificate.</param>
 2112    /// <returns>A certificate instance containing only the public certificate data.</returns>
 2113    private static X509Certificate2 CreatePublicOnlyCertificate(X509Certificate2 certificate)
 2114    {
 32115        ArgumentNullException.ThrowIfNull(certificate);
 2116
 2117#if NET9_0_OR_GREATER
 32118        return X509CertificateLoader.LoadCertificate(certificate.RawData);
 2119#else
 2120        return new X509Certificate2(certificate.RawData);
 2121#endif
 2122    }
 2123
 2124    /// <summary>
 2125    /// Validates development certificate creation options before certificate generation begins.
 2126    /// </summary>
 2127    /// <param name="options">The development certificate creation options.</param>
 2128    /// <exception cref="ArgumentOutOfRangeException">Thrown when an option falls outside the supported range.</exceptio
 2129    private static void ValidateDevelopmentCertificateOptions(SelfSignedOptions options)
 2130    {
 32131        if (options.RootValidDays is < 1 or > 36500)
 2132        {
 02133            throw new ArgumentOutOfRangeException(nameof(options.RootValidDays), options.RootValidDays, "RootValidDays m
 2134        }
 2135
 32136        if (options.LeafValidDays is < 1 or > 3650)
 2137        {
 02138            throw new ArgumentOutOfRangeException(nameof(options.LeafValidDays), options.LeafValidDays, "LeafValidDays m
 2139        }
 32140    }
 2141
 2142    /// <summary>
 2143    /// Resolves the enhanced key usages to emit for the generated certificate.
 2144    /// </summary>
 2145    /// <param name="options">The certificate generation options.</param>
 2146    /// <returns>An array of enhanced key usage OIDs to emit.</returns>
 2147    private static KeyPurposeID[] ResolveEnhancedKeyUsages(SelfSignedOptions options)
 2148    {
 2149        // If the caller explicitly specified EKU purposes, use those directly.
 272150        if (options.Purposes is not null)
 2151        {
 02152            return [.. options.Purposes];
 2153        }
 2154        // For CAs, it's common to omit EKU or use id-kp-certificateAuthority (
 2155        // which is not universally recognized), so we'll emit no EKU for CA certs to maximize compatibility.
 272156        if (options.IsCertificateAuthority)
 2157        {
 52158            return [];
 2159        }
 2160        // For end-entity certs, default to both server and client auth EKUs to maximize compatibility with various use 
 222161        return
 222162        [
 222163            KeyPurposeID.id_kp_serverAuth,
 222164            KeyPurposeID.id_kp_clientAuth
 222165        ];
 2166    }
 2167
 2168    /// <summary>
 2169    /// Resolves the key usage flags to emit for the generated certificate.
 2170    /// </summary>
 2171    /// <param name="options">The certificate generation options.</param>
 2172    /// <returns>The Bouncy Castle key usage bitmask.</returns>
 2173    private static int ResolveKeyUsage(SelfSignedOptions options)
 2174    {
 2175        // If the caller explicitly specified key usage flags, use those directly.
 272176        if (options.KeyUsageFlags is { } explicitKeyUsage && explicitKeyUsage != X509KeyUsageFlags.None)
 2177        {
 12178            return (int)explicitKeyUsage;
 2179        }
 2180
 2181        // For CAs, the key usage must include KeyCertSign. For end-entity certs, DigitalSignature is usually appropriat
 262182        if (options.IsCertificateAuthority)
 2183        {
 52184            return KeyUsage.KeyCertSign | KeyUsage.CrlSign;
 2185        }
 2186
 2187        // For end-entity certs, default to DigitalSignature, with KeyEncipherment added for RSA keys to support a wider
 212188        return options.KeyType == KeyType.Rsa
 212189            ? KeyUsage.DigitalSignature | KeyUsage.KeyEncipherment
 212190            : KeyUsage.DigitalSignature;
 2191    }
 2192
 2193    /// <summary>
 2194    /// Gets the certificate signature algorithm appropriate for the signing private key.
 2195    /// </summary>
 2196    /// <param name="signingKey">The private key used to sign the certificate.</param>
 2197    /// <returns>The Bouncy Castle signature algorithm name.</returns>
 2198    /// <exception cref="ArgumentOutOfRangeException">Thrown when the signing key algorithm is unsupported.</exception>
 2199    private static string GetSignatureAlgorithm(AsymmetricKeyParameter signingKey) =>
 272200        signingKey switch
 272201        {
 262202            RsaKeyParameters => "SHA256WITHRSA",
 12203            ECPrivateKeyParameters => "SHA384WITHECDSA",
 02204            _ => throw new ArgumentOutOfRangeException(nameof(signingKey), "Only RSA and ECDSA signing keys are supporte
 272205        };
 2206
 2207    /// <summary>
 2208    /// Gets the enhanced key usage purposes (EKU) from the specified X509 certificate.
 2209    /// </summary>
 2210    /// <param name="cert">The X509Certificate2 to extract purposes from.</param>
 2211    /// <returns>An enumerable of purpose names or OID values.</returns>
 2212    public static IEnumerable<string> GetPurposes(X509Certificate2 cert) =>
 12213        cert.Extensions
 12214            .OfType<X509EnhancedKeyUsageExtension>()
 12215            .SelectMany(x => x.EnhancedKeyUsages.Cast<Oid>())
 22216            .Select(o => (o.FriendlyName ?? o.Value)!)   // ← null-forgiving
 32217            .Where(s => s.Length > 0);                   // optional: drop empties
 2218    #endregion
 2219
 2220    #region  private helpers
 2221    private static AsymmetricCipherKeyPair GenRsaKeyPair(int bits, SecureRandom rng)
 2222    {
 292223        var gen = new RsaKeyPairGenerator();
 292224        gen.Init(new KeyGenerationParameters(rng, bits));
 292225        return gen.GenerateKeyPair();
 2226    }
 2227
 2228    /// <summary>
 2229    /// Generates an EC key pair.
 2230    /// </summary>
 2231    /// <param name="bits">The key size in bits.</param>
 2232    /// <param name="rng">The secure random number generator.</param>
 2233    /// <returns>The generated EC key pair.</returns>
 2234    private static AsymmetricCipherKeyPair GenEcKeyPair(int bits, SecureRandom rng)
 2235    {
 2236        // NIST-style names are fine here
 22237        var name = bits switch
 22238        {
 22239            <= 256 => "P-256",
 02240            <= 384 => "P-384",
 02241            _ => "P-521"
 22242        };
 2243
 2244        // Use the named-curve OID so PKCS#8/PKCS#12 encodes the EC key with named
 2245        // parameters instead of explicit curve parameters, which improves cross-platform
 2246        // import compatibility on macOS and Windows.
 22247        var curveOid = ECNamedCurveTable.GetOid(name)
 22248                       ?? throw new InvalidOperationException($"Curve OID not found: {name}");
 2249
 22250        var gen = new ECKeyPairGenerator();
 22251        gen.Init(new ECKeyGenerationParameters(curveOid, rng));
 22252        return gen.GenerateKeyPair();
 2253    }
 2254
 2255    /// <summary>
 2256    /// Converts a BouncyCastle X509Certificate to a .NET X509Certificate2.
 2257    /// </summary>
 2258    /// <param name="cert">The BouncyCastle X509Certificate to convert.</param>
 2259    /// <param name="privKey">The private key associated with the certificate.</param>
 2260    /// <param name="flags">The key storage flags to use.</param>
 2261    /// <param name="ephemeral">Whether the key is ephemeral.</param>
 2262    /// <returns></returns>
 2263    private static X509Certificate2 ToX509Cert2(
 2264        Org.BouncyCastle.X509.X509Certificate cert,
 2265        AsymmetricKeyParameter privKey,
 2266        X509KeyStorageFlags flags,
 2267        bool ephemeral)
 2268    {
 272269        var store = new Pkcs12StoreBuilder().Build();
 272270        var entry = new X509CertificateEntry(cert);
 2271        const string alias = "cert";
 272272        store.SetKeyEntry(alias, new AsymmetricKeyEntry(privKey),
 272273                          [entry]);
 2274
 272275        using var ms = new MemoryStream();
 2276        // Use null/default PKCS#12 password semantics consistently; macOS import is sensitive to
 2277        // empty-string passwords for unprotected PFX blobs generated by Bouncy Castle.
 272278        store.Save(ms, null, new SecureRandom());
 272279        var raw = ms.ToArray();
 2280
 2281#if NET9_0_OR_GREATER
 2282        try
 2283        {
 272284            var certificate = X509CertificateLoader.LoadPkcs12(
 272285                raw,
 272286                password: default,
 272287                keyStorageFlags: flags | (ephemeral ? X509KeyStorageFlags.EphemeralKeySet : 0),
 272288                loaderLimits: Pkcs12LoaderLimits.Defaults
 272289            );
 2290
 272291            return EnsureExportablePrivateKeySupport(
 272292                certificate,
 272293                exportable: flags.HasFlag(X509KeyStorageFlags.Exportable),
 272294                ephemeral: ephemeral,
 272295                reloadWithoutEphemeral: () => X509CertificateLoader.LoadPkcs12(
 272296                    raw,
 272297                    password: default,
 272298                    keyStorageFlags: flags,
 272299                    loaderLimits: Pkcs12LoaderLimits.Defaults));
 2300        }
 02301        catch (PlatformNotSupportedException) when (ephemeral)
 2302        {
 2303            // Some platforms (e.g. certain Linux/macOS runners) don't yet support
 2304            // EphemeralKeySet with the new X509CertificateLoader API. In that case
 2305            // we fall back to re-loading without the EphemeralKeySet flag. The
 2306            // intent of Ephemeral in our API is simply "do not persist in a store" –
 2307            // loading without the flag here still keeps the cert in-memory only.
 02308            Log.Debug("EphemeralKeySet not supported on this platform for X509CertificateLoader; falling back without th
 02309            return X509CertificateLoader.LoadPkcs12(
 02310                raw,
 02311                password: default,
 02312                keyStorageFlags: flags, // omit EphemeralKeySet
 02313                loaderLimits: Pkcs12LoaderLimits.Defaults
 02314            );
 2315        }
 2316#else
 2317        try
 2318        {
 2319            var certificate = new X509Certificate2(
 2320                raw,
 2321                (string?)null,
 2322                flags | (ephemeral ? X509KeyStorageFlags.EphemeralKeySet : 0)
 2323            );
 2324
 2325            return EnsureExportablePrivateKeySupport(
 2326                certificate,
 2327                exportable: flags.HasFlag(X509KeyStorageFlags.Exportable),
 2328                ephemeral: ephemeral,
 2329                reloadWithoutEphemeral: () => new X509Certificate2(
 2330                    raw,
 2331                    (string?)null,
 2332                    flags));
 2333        }
 2334        catch (PlatformNotSupportedException) when (ephemeral)
 2335        {
 2336            // macOS (and some Linux distros) under net8 may not support EphemeralKeySet here.
 2337            Log.Debug("EphemeralKeySet not supported on this platform (net8); falling back without the flag.");
 2338            return new X509Certificate2(
 2339                raw,
 2340                (string?)null,
 2341                flags // omit EphemeralKeySet
 2342            );
 2343        }
 2344
 2345#endif
 272346    }
 2347
 2348    /// <summary>
 2349    /// Reloads a certificate without <see cref="X509KeyStorageFlags.EphemeralKeySet"/> when an exportable private key c
 2350    /// </summary>
 2351    /// <param name="certificate">The loaded certificate to validate.</param>
 2352    /// <param name="exportable">Whether the caller requested an exportable private key.</param>
 2353    /// <param name="ephemeral">Whether the certificate was loaded with <see cref="X509KeyStorageFlags.EphemeralKeySet"/
 2354    /// <param name="reloadWithoutEphemeral">A callback that reloads the certificate without <see cref="X509KeyStorageFl
 2355    /// <returns>The original certificate when no fallback is required; otherwise, a reloaded certificate.</returns>
 2356    private static X509Certificate2 EnsureExportablePrivateKeySupport(
 2357        X509Certificate2 certificate,
 2358        bool exportable,
 2359        bool ephemeral,
 2360        Func<X509Certificate2> reloadWithoutEphemeral)
 2361    {
 272362        if (!ephemeral || !exportable || SupportsPkcs8PrivateKeyExport(certificate))
 2363        {
 272364            return certificate;
 2365        }
 2366
 02367        Log.Debug("EphemeralKeySet produced a non-exportable private key; reloading certificate without the flag.");
 02368        certificate.Dispose();
 02369        return reloadWithoutEphemeral();
 2370    }
 2371
 2372    /// <summary>
 2373    /// Determines whether the certificate's private key supports PKCS#8 export.
 2374    /// </summary>
 2375    /// <param name="certificate">The certificate to inspect.</param>
 2376    /// <returns><c>true</c> when the private key supports PKCS#8 export; otherwise, <c>false</c>.</returns>
 2377    private static bool SupportsPkcs8PrivateKeyExport(X509Certificate2 certificate)
 2378    {
 2379        try
 2380        {
 192381            if (certificate.GetRSAPrivateKey() is RSA rsa)
 2382            {
 182383                _ = rsa.ExportPkcs8PrivateKey();
 182384                return true;
 2385            }
 2386
 12387            if (certificate.GetECDsaPrivateKey() is ECDsa ecdsa)
 2388            {
 12389                _ = ecdsa.ExportPkcs8PrivateKey();
 12390                return true;
 2391            }
 02392        }
 02393        catch (CryptographicException)
 2394        {
 02395            return false;
 2396        }
 2397
 02398        return true;
 192399    }
 2400
 2401    /// <summary>
 2402    /// Encapsulates issuer details needed to sign a generated certificate.
 2403    /// </summary>
 2404    /// <param name="Subject">The issuer distinguished name.</param>
 2405    /// <param name="PrivateKey">The issuer private key.</param>
 2406    /// <param name="PublicKeyInfo">The issuer public key information.</param>
 42407    private sealed record IssuerMaterial(
 42408        X509Name Subject,
 42409        AsymmetricKeyParameter PrivateKey,
 82410        SubjectPublicKeyInfo PublicKeyInfo);
 2411
 2412    #endregion
 2413}

Methods/Properties

.cctor()
get_ShouldAppendKeyToPem()
NewSelfSigned(Kestrun.Certificates.SelfSignedOptions)
CreateSelfSignedCertificate(Kestrun.Certificates.SelfSignedOptions)
CreateDevelopmentCertificate(Kestrun.Certificates.SelfSignedOptions)
NewCertificateRequest(Kestrun.Certificates.CsrOptions,System.ReadOnlySpan`1<System.Char>)
Add()
Import(System.String,System.ReadOnlySpan`1<System.Char>,System.String,System.Security.Cryptography.X509Certificates.X509KeyStorageFlags)
ValidateImportInputs(System.String,System.String)
ImportPfx(System.String,System.ReadOnlySpan`1<System.Char>,System.Security.Cryptography.X509Certificates.X509KeyStorageFlags)
ImportDer(System.String)
ImportPem(System.String,System.ReadOnlySpan`1<System.Char>,System.String)
ImportPemUnencrypted(System.String,System.String)
ImportPemEncrypted(System.String,System.ReadOnlySpan`1<System.Char>,System.String)
TryManualEncryptedPemPairing(System.String,System.ReadOnlySpan`1<System.Char>,System.String,System.Security.Cryptography.X509Certificates.X509Certificate2&)
ExtractEncryptedPemDer(System.String)
TryPairCertificateWithKey(System.Security.Cryptography.X509Certificates.X509Certificate2,System.ReadOnlySpan`1<System.Char>,System.Byte[],System.Security.Cryptography.X509Certificates.X509Certificate2&)
TryPairWithRsa(System.Security.Cryptography.X509Certificates.X509Certificate2,System.ReadOnlySpan`1<System.Char>,System.Byte[],System.Int32,System.Security.Cryptography.X509Certificates.X509Certificate2&,System.Exception&)
TryPairWithEcdsa(System.Security.Cryptography.X509Certificates.X509Certificate2,System.ReadOnlySpan`1<System.Char>,System.Byte[],System.Int32,System.Security.Cryptography.X509Certificates.X509Certificate2&,System.Exception&)
LoadCertOnlyPem(System.String)
Import(System.String,System.Security.SecureString,System.String,System.Security.Cryptography.X509Certificates.X509KeyStorageFlags)
Import(System.String,System.String,System.Security.Cryptography.X509Certificates.X509KeyStorageFlags)
Import(System.String)
Export(System.Security.Cryptography.X509Certificates.X509Certificate2,System.String,Kestrun.Certificates.ExportFormat,System.ReadOnlySpan`1<System.Char>,System.Boolean)
NormalizeExportPath(System.String,Kestrun.Certificates.ExportFormat)
EnsureOutputDirectoryExists(System.String)
.ctor(System.Security.SecureString,System.Char[])
get_Secure()
get_Chars()
Dispose()
CreatePasswordShapes(System.ReadOnlySpan`1<System.Char>)
ExportPfx(System.Security.Cryptography.X509Certificates.X509Certificate2,System.String,System.Security.SecureString)
ExportPem(System.Security.Cryptography.X509Certificates.X509Certificate2,System.String,System.ReadOnlySpan`1<System.Char>,System.Boolean)
WritePrivateKey(System.Security.Cryptography.X509Certificates.X509Certificate2,System.ReadOnlySpan`1<System.Char>,System.String)
Export(System.Security.Cryptography.X509Certificates.X509Certificate2,System.String,Kestrun.Certificates.ExportFormat,System.Security.SecureString,System.Boolean)
ExportPemFromJwkJson(System.String,System.String,System.ReadOnlySpan`1<System.Char>,System.Boolean)
ExportPemFromJwkJson(System.String,System.String,System.Security.SecureString,System.Boolean)
CreateSelfSignedCertificateFromJwk(System.String,System.String)
BuildPrivateKeyJwt(Microsoft.IdentityModel.Tokens.SecurityKey,System.String,System.String)
BuildPrivateKeyJwt(System.Security.Cryptography.X509Certificates.X509Certificate2,System.String,System.String)
BuildPrivateKeyJwtFromJwkJson(System.String,System.String,System.String)
CreateJwkJsonFromCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Boolean)
CreateJwkJsonFromRsa(System.Security.Cryptography.RSA,System.String)
CreateJwkJsonFromRsaPrivateKeyPem(System.String,System.String)
Validate(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Boolean,System.Boolean,System.Boolean,System.Security.Cryptography.OidCollection,System.Boolean)
Validate(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Boolean,System.Boolean,System.Boolean,System.Security.Cryptography.OidCollection,System.Boolean,System.Security.Cryptography.X509Certificates.X509Certificate2Collection)
Validate(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Boolean,System.Boolean,System.Boolean,System.Security.Cryptography.OidCollection,System.Boolean,System.String&)
Validate(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Boolean,System.Boolean,System.Boolean,System.Security.Cryptography.OidCollection,System.Boolean,System.Security.Cryptography.X509Certificates.X509Certificate2Collection,System.String&)
IsWithinValidityPeriod(System.Security.Cryptography.X509Certificates.X509Certificate2)
BuildChainOk(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Boolean,System.Boolean,System.Security.Cryptography.X509Certificates.X509Certificate2Collection,System.String&)
BuildChainOk(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Security.Cryptography.X509Certificates.X509Certificate2Collection,System.String&)
CreateValidationChain(System.Boolean,System.Security.Cryptography.X509Certificates.X509Certificate2,System.Boolean,System.Security.Cryptography.X509Certificates.X509Certificate2Collection)
ConfigureValidationChainPolicy(System.Security.Cryptography.X509Certificates.X509Chain,System.Security.Cryptography.X509Certificates.X509Certificate2,System.Boolean,System.Security.Cryptography.X509Certificates.X509Certificate2Collection)
GetRootCertificates(System.Security.Cryptography.X509Certificates.X509Certificate2Collection,System.Security.Cryptography.X509Certificates.X509Certificate2)
GetIntermediateCertificates(System.Security.Cryptography.X509Certificates.X509Certificate2Collection,System.Security.Cryptography.X509Certificates.X509Certificate2)
CertificateMatches(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Security.Cryptography.X509Certificates.X509Certificate2)
IsSelfSignedCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2)
BuildChainFailureReason(System.String,System.Boolean)
PurposesOk(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Security.Cryptography.OidCollection,System.Boolean)
NormalizeExpectedPurposeOids(System.Security.Cryptography.OidCollection)
GetEkuOids(System.Security.Cryptography.X509Certificates.X509Certificate2)
UsesWeakAlgorithms(System.Security.Cryptography.X509Certificates.X509Certificate2)
GetWeakAlgorithmFindings(System.Security.Cryptography.X509Certificates.X509Certificate2)
DescribeCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2)
DescribeExpectedPurposes(System.Security.Cryptography.OidCollection)
DescribeChainStatuses(System.Security.Cryptography.X509Certificates.X509ChainStatus[])
ResolveIssuerMaterial(System.Security.Cryptography.X509Certificates.X509Certificate2)
ValidateIssuerCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2)
GetPrivateKeyParameter(System.Security.Cryptography.X509Certificates.X509Certificate2)
GetPrivateKeyParameter(System.Security.Cryptography.RSA)
GetPrivateKeyParameter(System.Security.Cryptography.ECDsa)
CreateDevelopmentRoot(Kestrun.Certificates.SelfSignedOptions)
TrustDevelopmentRootIfRequested(Kestrun.Certificates.SelfSignedOptions,System.Security.Cryptography.X509Certificates.X509Certificate2)
CreatePublicOnlyCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2)
ValidateDevelopmentCertificateOptions(Kestrun.Certificates.SelfSignedOptions)
ResolveEnhancedKeyUsages(Kestrun.Certificates.SelfSignedOptions)
ResolveKeyUsage(Kestrun.Certificates.SelfSignedOptions)
GetSignatureAlgorithm(Org.BouncyCastle.Crypto.AsymmetricKeyParameter)
GetPurposes(System.Security.Cryptography.X509Certificates.X509Certificate2)
GenRsaKeyPair(System.Int32,Org.BouncyCastle.Security.SecureRandom)
GenEcKeyPair(System.Int32,Org.BouncyCastle.Security.SecureRandom)
ToX509Cert2(Org.BouncyCastle.X509.X509Certificate,Org.BouncyCastle.Crypto.AsymmetricKeyParameter,System.Security.Cryptography.X509Certificates.X509KeyStorageFlags,System.Boolean)
EnsureExportablePrivateKeySupport(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Boolean,System.Boolean,System.Func`1<System.Security.Cryptography.X509Certificates.X509Certificate2>)
SupportsPkcs8PrivateKeyExport(System.Security.Cryptography.X509Certificates.X509Certificate2)
.ctor(Org.BouncyCastle.Asn1.X509.X509Name,Org.BouncyCastle.Crypto.AsymmetricKeyParameter,Org.BouncyCastle.Asn1.X509.SubjectPublicKeyInfo)
get_Subject()
get_PrivateKey()
get_PublicKeyInfo()