< 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@eeafbe813231ed23417e7b339e170e307b2c86f9
Line coverage
53%
Covered lines: 321
Uncovered lines: 276
Coverable lines: 597
Total lines: 1604
Line coverage: 53.7%
Branch coverage
47%
Covered branches: 118
Total branches: 250
Branch coverage: 47.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 09/12/2025 - 13:32:05 Line coverage: 73.5% (311/423) Branch coverage: 57.2% (103/180) Total lines: 1164 Tag: Kestrun/Kestrun@63ea5841fe73fd164406accba17a956e8c08357f10/13/2025 - 16:52:37 Line coverage: 71.4% (311/435) Branch coverage: 55.9% (103/184) Total lines: 1227 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/19/2025 - 02:25:56 Line coverage: 52.7% (301/571) Branch coverage: 47.3% (107/226) Total lines: 1529 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/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@bf8a937cfb7e8936c225b9df4608f8ddd85558b1 09/12/2025 - 13:32:05 Line coverage: 73.5% (311/423) Branch coverage: 57.2% (103/180) Total lines: 1164 Tag: Kestrun/Kestrun@63ea5841fe73fd164406accba17a956e8c08357f10/13/2025 - 16:52:37 Line coverage: 71.4% (311/435) Branch coverage: 55.9% (103/184) Total lines: 1227 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/19/2025 - 02:25:56 Line coverage: 52.7% (301/571) Branch coverage: 47.3% (107/226) Total lines: 1529 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/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@bf8a937cfb7e8936c225b9df4608f8ddd85558b1

Coverage delta

Coverage delta 19 -19

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_ShouldAppendKeyToPem()50%22100%
NewSelfSigned(...)58.33%121295.23%
NewCertificateRequest(...)75%131284.05%
Add()100%22100%
Import(...)83.33%121288.88%
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.88%
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%
.cctor()100%210%
CreateSelfSignedCertificateFromJwk(...)0%2040%
BuildPrivateKeyJwt(...)100%210%
BuildPrivateKeyJwt(...)100%210%
BuildPrivateKeyJwtFromJwkJson(...)100%210%
CreateJwkJsonFromCertificate(...)0%7280%
CreateJwkJsonFromRsa(...)0%156120%
CreateJwkJsonFromRsaPrivateKeyPem(...)0%620%
Validate(...)80%101084.61%
IsWithinValidityPeriod(...)50%66100%
BuildChainOk(...)33.33%231257.14%
BuildChainOk(...)100%22100%
PurposesOk(...)80%1010100%
GetEkuOids(...)75%8890%
UsesWeakAlgorithms(...)50%1010100%
GetPurposes(...)50%22100%
GenRsaKeyPair(...)100%11100%
GenEcKeyPair(...)33.33%6684.61%
ToX509Cert2(...)100%2266.66%

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 System.Text;
 20using Org.BouncyCastle.Asn1.X9;
 21using Serilog;
 22using Kestrun.Utilities;
 23using System.Text.Json;
 24using Microsoft.IdentityModel.Tokens;
 25using System.Security.Claims;
 26using Microsoft.IdentityModel.JsonWebTokens;
 27using System.Text.Json.Serialization;
 28
 29namespace Kestrun.Certificates;
 30
 31/// <summary>
 32/// Drop-in replacement for Pode’s certificate helpers, powered by Bouncy Castle.
 33/// </summary>
 34public static class CertificateManager
 35{
 36    /// <summary>
 37    /// Controls whether the private key is appended to the certificate PEM file in addition to
 38    /// writing a separate .key file. Appending was initially added to work around platform
 39    /// inconsistencies when importing encrypted PEM pairs on some Linux runners. However, having
 40    /// both a combined (cert+key) file and a separate key file can itself introduce ambiguity in
 41    /// which API path <see cref="X509Certificate2"/> chooses (single-file vs dual-file), which was
 42    /// observed to contribute to rare flakiness (private key occasionally not attached after
 43    /// import). To make behavior deterministic we now disable appending by default and allow it to
 44    /// be re-enabled explicitly via the environment variable KESTRUN_APPEND_KEY_TO_PEM.
 45    /// Set KESTRUN_APPEND_KEY_TO_PEM=1 (or "true") to re-enable.
 46    /// </summary>
 47    private static bool ShouldAppendKeyToPem =>
 248        string.Equals(Environment.GetEnvironmentVariable("KESTRUN_APPEND_KEY_TO_PEM"), "1", StringComparison.OrdinalIgno
 249        string.Equals(Environment.GetEnvironmentVariable("KESTRUN_APPEND_KEY_TO_PEM"), "true", StringComparison.OrdinalI
 50
 51    #region  Self-signed certificate
 52    /// <summary>
 53    /// Creates a new self-signed X509 certificate using the specified options.
 54    /// </summary>
 55    /// <param name="o">Options for creating the self-signed certificate.</param>
 56    /// <returns>A new self-signed X509Certificate2 instance.</returns>
 57    public static X509Certificate2 NewSelfSigned(SelfSignedOptions o)
 58    {
 1259        var random = new SecureRandom(new CryptoApiRandomGenerator());
 60
 61        // ── 1. Key pair ───────────────────────────────────────────────────────────
 1262        var keyPair =
 1263            o.KeyType switch
 1264            {
 1265                KeyType.Rsa => GenRsaKeyPair(o.KeyLength, random),
 066                KeyType.Ecdsa => GenEcKeyPair(o.KeyLength, random),
 067                _ => throw new ArgumentOutOfRangeException()
 1268            };
 69
 70        // ── 2. Certificate body ───────────────────────────────────────────────────
 1271        var notBefore = DateTime.UtcNow.AddMinutes(-5);
 1272        var notAfter = notBefore.AddDays(o.ValidDays);
 1273        var serial = BigIntegers.CreateRandomInRange(
 1274                            BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
 75
 1276        var subjectDn = new X509Name($"CN={o.DnsNames.First()}");
 1277        var gen = new X509V3CertificateGenerator();
 1278        gen.SetSerialNumber(serial);
 1279        gen.SetIssuerDN(subjectDn);
 1280        gen.SetSubjectDN(subjectDn);
 1281        gen.SetNotBefore(notBefore);
 1282        gen.SetNotAfter(notAfter);
 1283        gen.SetPublicKey(keyPair.Public);
 84
 85        // SANs
 1286        var altNames = o.DnsNames
 2387                        .Select(n => new GeneralName(
 2388                            IPAddress.TryParse(n, out _) ?
 2389                                GeneralName.IPAddress : GeneralName.DnsName, n))
 1290                        .ToArray();
 1291        gen.AddExtension(X509Extensions.SubjectAlternativeName, false,
 1292                         new DerSequence(altNames));
 93
 94        // EKU
 1295        var eku = o.Purposes ??
 1296         [
 1297             KeyPurposeID.id_kp_serverAuth,
 1298            KeyPurposeID.id_kp_clientAuth
 1299         ];
 12100        gen.AddExtension(X509Extensions.ExtendedKeyUsage, false,
 12101                         new ExtendedKeyUsage([.. eku]));
 102
 103        // KeyUsage – allow digitalSignature & keyEncipherment
 12104        gen.AddExtension(X509Extensions.KeyUsage, true,
 12105                         new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.KeyEncipherment));
 106
 107        // ── 3. Sign & output ──────────────────────────────────────────────────────
 12108        var sigAlg = o.KeyType == KeyType.Rsa ? "SHA256WITHRSA" : "SHA384WITHECDSA";
 12109        var signer = new Asn1SignatureFactory(sigAlg, keyPair.Private, random);
 12110        var cert = gen.Generate(signer);
 111
 12112        return ToX509Cert2(cert, keyPair.Private,
 12113            o.Exportable ? X509KeyStorageFlags.Exportable : X509KeyStorageFlags.DefaultKeySet,
 12114            o.Ephemeral);
 115    }
 116    #endregion
 117
 118    #region  CSR
 119
 120    /// <summary>
 121    /// Creates a new Certificate Signing Request (CSR) and returns the PEM-encoded CSR and the private key.
 122    /// </summary>
 123    /// <param name="options">The options for the CSR.</param>
 124    /// <param name="encryptionPassword">The password to encrypt the private key, if desired.</param>
 125    /// <returns>A <see cref="CsrResult"/> containing the CSR and private key information.</returns>
 126    /// <exception cref="ArgumentOutOfRangeException"></exception>
 127    public static CsrResult NewCertificateRequest(CsrOptions options, ReadOnlySpan<char> encryptionPassword = default)
 128    {
 129        // 0️⃣ Keypair
 2130        var random = new SecureRandom(new CryptoApiRandomGenerator());
 2131        var keyPair = options.KeyType switch
 2132        {
 1133            KeyType.Rsa => GenRsaKeyPair(options.KeyLength, random),
 1134            KeyType.Ecdsa => GenEcKeyPair(options.KeyLength, random),
 0135            _ => throw new ArgumentOutOfRangeException(nameof(options.KeyType))
 2136        };
 137
 138        // 1️⃣ Subject DN
 2139        var order = new List<DerObjectIdentifier>();
 2140        var attrs = new Dictionary<DerObjectIdentifier, string>();
 141        void Add(DerObjectIdentifier oid, string? v)
 142        {
 12143            if (!string.IsNullOrWhiteSpace(v)) { order.Add(oid); attrs[oid] = v; }
 8144        }
 2145        Add(X509Name.C, options.Country);
 2146        Add(X509Name.O, options.Org);
 2147        Add(X509Name.OU, options.OrgUnit);
 2148        Add(X509Name.CN, options.CommonName ?? options.DnsNames.First());
 2149        var subject = new X509Name(order, attrs);
 150
 151        // 2️⃣ SAN extension
 2152        var altNames = options.DnsNames
 3153            .Select(d => new GeneralName(
 3154                IPAddress.TryParse(d, out _)
 3155                    ? GeneralName.IPAddress
 3156                    : GeneralName.DnsName, d))
 2157            .ToArray();
 2158        var sanSeq = new DerSequence(altNames);
 159
 2160        var extGen = new X509ExtensionsGenerator();
 2161        extGen.AddExtension(X509Extensions.SubjectAlternativeName, false, sanSeq);
 2162        var extensions = extGen.Generate();
 163
 2164        var extensionRequestAttr = new AttributePkcs(
 2165            PkcsObjectIdentifiers.Pkcs9AtExtensionRequest,
 2166            new DerSet(extensions));
 2167        var attrSet = new DerSet(extensionRequestAttr);
 168
 169        // 3️⃣ CSR
 2170        var sigAlg = options.KeyType == KeyType.Rsa ? "SHA256WITHRSA" : "SHA384WITHECDSA";
 2171        var csr = new Pkcs10CertificationRequest(sigAlg, subject, keyPair.Public, attrSet, keyPair.Private);
 172
 173        // 4️⃣ CSR PEM + DER
 174        string csrPem;
 2175        using (var sw = new StringWriter())
 176        {
 2177            new PemWriter(sw).WriteObject(csr);
 2178            csrPem = sw.ToString();
 2179        }
 2180        var csrDer = csr.GetEncoded();
 181
 182        // 5️⃣ Private key PEM + DER
 183        string privateKeyPem;
 2184        using (var sw = new StringWriter())
 185        {
 2186            new PemWriter(sw).WriteObject(keyPair.Private);
 2187            privateKeyPem = sw.ToString();
 2188        }
 2189        var pkInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(keyPair.Private);
 2190        var privateKeyDer = pkInfo.GetEncoded();
 191
 192        // 6️⃣ Optional encrypted PEM
 2193        string? privateKeyPemEncrypted = null;
 2194        if (!encryptionPassword.IsEmpty)
 195        {
 0196            var pwd = encryptionPassword.ToArray(); // BC requires char[]
 197            try
 198            {
 0199                var gen = new Pkcs8Generator(keyPair.Private, Pkcs8Generator.PbeSha1_3DES)
 0200                {
 0201                    Password = pwd
 0202                };
 0203                using var encSw = new StringWriter();
 0204                new PemWriter(encSw).WriteObject(gen);
 0205                privateKeyPemEncrypted = encSw.ToString();
 206            }
 207            finally
 208            {
 0209                Array.Clear(pwd, 0, pwd.Length); // wipe memory
 0210            }
 211        }
 212
 213        // 7️⃣ Public key PEM + DER
 2214        var spki = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(keyPair.Public);
 2215        var publicKeyDer = spki.GetEncoded();
 216        string publicKeyPem;
 2217        using (var sw = new StringWriter())
 218        {
 2219            new PemWriter(sw).WriteObject(spki);
 2220            publicKeyPem = sw.ToString();
 2221        }
 222
 2223        return new CsrResult(
 2224            csrPem,
 2225            csrDer,
 2226            keyPair.Private,
 2227            privateKeyPem,
 2228            privateKeyDer,
 2229            privateKeyPemEncrypted,
 2230            publicKeyPem,
 2231            publicKeyDer
 2232        );
 233    }
 234
 235    #endregion
 236
 237    #region  Import
 238    /// <summary>
 239    /// Imports an X509 certificate from the specified file path, with optional password and private key file.
 240    /// </summary>
 241    /// <param name="certPath">The path to the certificate file.</param>
 242    /// <param name="password">The password for the certificate, if required.</param>
 243    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 244    /// <param name="flags">Key storage flags for the imported certificate.</param>
 245    /// <returns>The imported X509Certificate2 instance.</returns>
 246    public static X509Certificate2 Import(
 247       string certPath,
 248       ReadOnlySpan<char> password = default,
 249       string? privateKeyPath = null,
 250       X509KeyStorageFlags flags = X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.Exportable)
 251    {
 9252        ValidateImportInputs(certPath, privateKeyPath);
 253
 6254        var ext = Path.GetExtension(certPath).ToLowerInvariant();
 6255        return ext switch
 6256        {
 2257            ".pfx" or ".p12" => ImportPfx(certPath, password, flags),
 1258            ".cer" or ".der" => ImportDer(certPath),
 3259            ".pem" or ".crt" => ImportPem(certPath, password, privateKeyPath),
 0260            _ => throw new NotSupportedException($"Certificate extension '{ext}' is not supported.")
 6261        };
 262    }
 263
 264    /// <summary>
 265    /// Validates the inputs for importing a certificate.
 266    /// </summary>
 267    /// <param name="certPath">The path to the certificate file.</param>
 268    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 269    private static void ValidateImportInputs(string certPath, string? privateKeyPath)
 270    {
 9271        if (string.IsNullOrEmpty(certPath))
 272        {
 1273            throw new ArgumentException("Certificate path cannot be null or empty.", nameof(certPath));
 274        }
 8275        if (!File.Exists(certPath))
 276        {
 1277            throw new FileNotFoundException("Certificate file not found.", certPath);
 278        }
 7279        if (!string.IsNullOrEmpty(privateKeyPath) && !File.Exists(privateKeyPath))
 280        {
 1281            throw new FileNotFoundException("Private key file not found.", privateKeyPath);
 282        }
 6283    }
 284
 285    /// <summary>
 286    /// Imports a PFX certificate from the specified file path.
 287    /// </summary>
 288    /// <param name="certPath">The path to the certificate file.</param>
 289    /// <param name="password">The password for the certificate, if required.</param>
 290    /// <param name="flags">Key storage flags for the imported certificate.</param>
 291    /// <returns>The imported X509Certificate2 instance.</returns>
 292    private static X509Certificate2 ImportPfx(string certPath, ReadOnlySpan<char> password, X509KeyStorageFlags flags)
 293#if NET9_0_OR_GREATER
 294        => X509CertificateLoader.LoadPkcs12FromFile(certPath, password, flags, Pkcs12LoaderLimits.Defaults);
 295#else
 2296        => new(File.ReadAllBytes(certPath), password, flags);
 297#endif
 298
 299    private static X509Certificate2 ImportDer(string certPath)
 300#if NET9_0_OR_GREATER
 301        => X509CertificateLoader.LoadCertificateFromFile(certPath);
 302#else
 1303        => new(File.ReadAllBytes(certPath));
 304#endif
 305
 306    /// <summary>
 307    /// Imports a PEM certificate from the specified file path.
 308    /// </summary>
 309    /// <param name="certPath">The path to the certificate file.</param>
 310    /// <param name="password">The password for the certificate, if required.</param>
 311    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 312    /// <returns>The imported X509Certificate2 instance.</returns>
 313    private static X509Certificate2 ImportPem(string certPath, ReadOnlySpan<char> password, string? privateKeyPath)
 314    {
 315        // No separate key file provided
 3316        if (string.IsNullOrEmpty(privateKeyPath))
 317        {
 1318            return password.IsEmpty
 1319                ? LoadCertOnlyPem(certPath)
 1320                : X509Certificate2.CreateFromEncryptedPemFile(certPath, password);
 321        }
 322
 323        // Separate key file provided
 2324        return password.IsEmpty
 2325            ? ImportPemUnencrypted(certPath, privateKeyPath)
 2326            : ImportPemEncrypted(certPath, password, privateKeyPath);
 327    }
 328
 329    /// <summary>
 330    /// Imports an unencrypted PEM certificate from the specified file path.
 331    /// </summary>
 332    /// <param name="certPath">The path to the certificate file.</param>
 333    /// <param name="privateKeyPath">The path to the private key file.</param>
 334    /// <returns>The imported X509Certificate2 instance.</returns>
 335    private static X509Certificate2 ImportPemUnencrypted(string certPath, string privateKeyPath)
 1336        => X509Certificate2.CreateFromPemFile(certPath, privateKeyPath);
 337
 338    /// <summary>
 339    /// Imports a PEM certificate from the specified file path.
 340    /// </summary>
 341    /// <param name="certPath">The path to the certificate file.</param>
 342    /// <param name="password">The password for the certificate, if required.</param>
 343    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 344    /// <returns>The imported X509Certificate2 instance.</returns>
 345    private static X509Certificate2 ImportPemEncrypted(string certPath, ReadOnlySpan<char> password, string privateKeyPa
 346    {
 347        // Prefer single-file path (combined) first for reliability on some platforms
 348        try
 349        {
 1350            var single = X509Certificate2.CreateFromEncryptedPemFile(certPath, password);
 0351            if (single.HasPrivateKey)
 352            {
 0353                Log.Debug("Imported encrypted PEM using single-file path (combined cert+key) for {CertPath}", certPath);
 0354                return single;
 355            }
 0356        }
 1357        catch (Exception exSingle)
 358        {
 1359            Log.Debug(exSingle, "Single-file encrypted PEM import failed, falling back to separate key file {KeyFile}", 
 1360        }
 361
 1362        var loaded = X509Certificate2.CreateFromEncryptedPemFile(certPath, password, privateKeyPath);
 363
 1364        if (loaded.HasPrivateKey)
 365        {
 1366            return loaded;
 367        }
 368
 369        // Fallback manual pairing if platform failed to associate the key
 0370        TryManualEncryptedPemPairing(certPath, password, privateKeyPath, ref loaded);
 0371        return loaded;
 0372    }
 373
 374    /// <summary>
 375    /// Tries to manually pair an encrypted PEM certificate with its private key.
 376    /// </summary>
 377    /// <param name="certPath">The path to the certificate file.</param>
 378    /// <param name="password">The password for the certificate, if required.</param>
 379    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 380    /// <param name="loaded">The loaded X509Certificate2 instance.</param>
 381    private static void TryManualEncryptedPemPairing(string certPath, ReadOnlySpan<char> password, string privateKeyPath
 382    {
 383        try
 384        {
 0385            var certOnly = LoadCertOnlyPem(certPath);
 0386            var encDer = ExtractEncryptedPemDer(privateKeyPath);
 387
 0388            if (encDer is null)
 389            {
 0390                Log.Debug("Encrypted PEM manual pairing fallback skipped: markers not found in key file {KeyFile}", priv
 0391                return;
 392            }
 393
 0394            var lastErr = TryPairCertificateWithKey(certOnly, password, encDer, ref loaded);
 395
 0396            if (lastErr != null)
 397            {
 0398                Log.Debug(lastErr, "Encrypted PEM manual pairing attempts failed (all rounds); returning original loaded
 399            }
 0400        }
 0401        catch (Exception ex)
 402        {
 0403            Log.Debug(ex, "Encrypted PEM manual pairing fallback failed unexpectedly; returning original loaded certific
 0404        }
 0405    }
 406
 407    /// <summary>
 408    /// Extracts the encrypted PEM DER bytes from a private key file.
 409    /// </summary>
 410    /// <param name="privateKeyPath">The path to the private key file.</param>
 411    /// <returns>The DER bytes if successful, null otherwise.</returns>
 412    private static byte[]? ExtractEncryptedPemDer(string privateKeyPath)
 413    {
 414        const string encBegin = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
 415        const string encEnd = "-----END ENCRYPTED PRIVATE KEY-----";
 416
 0417        byte[]? encDer = null;
 0418        for (var attempt = 0; attempt < 5 && encDer is null; attempt++)
 419        {
 0420            var keyPem = File.ReadAllText(privateKeyPath);
 0421            var start = keyPem.IndexOf(encBegin, StringComparison.Ordinal);
 0422            var end = keyPem.IndexOf(encEnd, StringComparison.Ordinal);
 0423            if (start >= 0 && end > start)
 424            {
 0425                start += encBegin.Length;
 0426                var b64 = keyPem[start..end].Replace("\r", "").Replace("\n", "").Trim();
 0427                try { encDer = Convert.FromBase64String(b64); }
 0428                catch (FormatException fe)
 429                {
 0430                    Log.Debug(fe, "Base64 decode failed on attempt {Attempt} reading encrypted key; retrying", attempt +
 0431                }
 432            }
 0433            if (encDer is null)
 434            {
 0435                Thread.Sleep(40 * (attempt + 1));
 436            }
 437        }
 438
 0439        return encDer;
 440    }
 441
 442    /// <summary>
 443    /// Attempts to pair a certificate with an encrypted private key using RSA and ECDSA.
 444    /// </summary>
 445    /// <param name="certOnly">The certificate without a private key.</param>
 446    /// <param name="password">The password for the encrypted key.</param>
 447    /// <param name="encDer">The encrypted DER bytes.</param>
 448    /// <param name="loaded">The loaded certificate (updated if pairing succeeds).</param>
 449    /// <returns>The last exception encountered, or null if pairing succeeded.</returns>
 450    private static Exception? TryPairCertificateWithKey(X509Certificate2 certOnly, ReadOnlySpan<char> password, byte[] e
 451    {
 0452        Exception? lastErr = null;
 0453        for (var round = 0; round < 2; round++)
 454        {
 0455            if (TryPairWithRsa(certOnly, password, encDer, round, ref loaded, ref lastErr))
 456            {
 0457                return null;
 458            }
 459
 0460            if (TryPairWithEcdsa(certOnly, password, encDer, round, ref loaded, ref lastErr))
 461            {
 0462                return null;
 463            }
 464
 0465            Thread.Sleep(25 * (round + 1));
 466        }
 0467        return lastErr;
 468    }
 469
 470    /// <summary>
 471    /// Tries to pair a certificate with an RSA private key.
 472    /// </summary>
 473    /// <param name="certOnly">The certificate without a private key.</param>
 474    /// <param name="password">The password for the encrypted key.</param>
 475    /// <param name="encDer">The encrypted DER bytes.</param>
 476    /// <param name="round">The attempt round number.</param>
 477    /// <param name="loaded">The loaded certificate (updated if pairing succeeds).</param>
 478    /// <param name="lastErr">The last exception encountered (updated on failure).</param>
 479    /// <returns>True if pairing succeeded, false otherwise.</returns>
 480    private static bool TryPairWithRsa(X509Certificate2 certOnly, ReadOnlySpan<char> password, byte[] encDer, int round,
 481    {
 482        try
 483        {
 0484            using var rsa = RSA.Create();
 0485            rsa.ImportEncryptedPkcs8PrivateKey(password, encDer, out _);
 0486            var withKey = certOnly.CopyWithPrivateKey(rsa);
 0487            if (withKey.HasPrivateKey)
 488            {
 0489                Log.Debug("Encrypted PEM manual pairing succeeded with RSA private key (round {Round}).", round + 1);
 0490                loaded = withKey;
 0491                return true;
 492            }
 0493        }
 0494        catch (Exception exRsa)
 495        {
 0496            lastErr = lastErr is null ? exRsa : new AggregateException(lastErr, exRsa);
 0497        }
 0498        return false;
 0499    }
 500
 501    /// <summary>
 502    /// Tries to pair a certificate with an ECDSA private key.
 503    /// </summary>
 504    /// <param name="certOnly">The certificate without a private key.</param>
 505    /// <param name="password">The password for the encrypted key.</param>
 506    /// <param name="encDer">The encrypted DER bytes.</param>
 507    /// <param name="round">The attempt round number.</param>
 508    /// <param name="loaded">The loaded certificate (updated if pairing succeeds).</param>
 509    /// <param name="lastErr">The last exception encountered (updated on failure).</param>
 510    /// <returns>True if pairing succeeded, false otherwise.</returns>
 511    private static bool TryPairWithEcdsa(X509Certificate2 certOnly, ReadOnlySpan<char> password, byte[] encDer, int roun
 512    {
 513        try
 514        {
 0515            using var ecdsa = ECDsa.Create();
 0516            ecdsa.ImportEncryptedPkcs8PrivateKey(password, encDer, out _);
 0517            var withKey = certOnly.CopyWithPrivateKey(ecdsa);
 0518            if (withKey.HasPrivateKey)
 519            {
 0520                Log.Debug("Encrypted PEM manual pairing succeeded with ECDSA private key (round {Round}).", round + 1);
 0521                loaded = withKey;
 0522                return true;
 523            }
 0524        }
 0525        catch (Exception exEc)
 526        {
 0527            lastErr = lastErr is null ? exEc : new AggregateException(lastErr, exEc);
 0528        }
 0529        return false;
 0530    }
 531
 532    /// <summary>
 533    /// Loads a certificate from a PEM file that contains *only* a CERTIFICATE block (no key).
 534    /// </summary>
 535    /// <param name="certPath">The path to the certificate file.</param>
 536    /// <returns>The loaded X509Certificate2 instance.</returns>
 537    private static X509Certificate2 LoadCertOnlyPem(string certPath)
 538    {
 539        // 1) Read + trim the whole PEM text
 1540        var pem = File.ReadAllText(certPath).Trim();
 541
 542        // 2) Define the BEGIN/END markers
 543        const string begin = "-----BEGIN CERTIFICATE-----";
 544        const string end = "-----END CERTIFICATE-----";
 545
 546        // 3) Find their positions
 1547        var start = pem.IndexOf(begin, StringComparison.Ordinal);
 1548        if (start < 0)
 549        {
 0550            throw new InvalidDataException("BEGIN CERTIFICATE marker not found");
 551        }
 552
 1553        start += begin.Length;
 554
 1555        var stop = pem.IndexOf(end, start, StringComparison.Ordinal);
 1556        if (stop < 0)
 557        {
 0558            throw new InvalidDataException("END CERTIFICATE marker not found");
 559        }
 560
 561        // 4) Extract, clean, and decode the Base64 payload
 1562        var b64 = pem[start..stop]
 1563                       .Replace("\r", "")
 1564                       .Replace("\n", "")
 1565                       .Trim();
 1566        var der = Convert.FromBase64String(b64);
 567
 568        // 5) Return the X509Certificate2
 569
 570#if NET9_0_OR_GREATER
 571        return X509CertificateLoader.LoadCertificate(der);
 572#else
 573        // .NET 8 or earlier path, using X509Certificate2 ctor
 574        // Note: this will not work in .NET 9+ due to the new X509CertificateLoader API
 575        //       which requires a byte array or a file path.
 1576        return new X509Certificate2(der);
 577#endif
 578    }
 579
 580    /// <summary>
 581    /// Imports an X509 certificate from the specified file path, using a SecureString password and optional private key
 582    /// </summary>
 583    /// <param name="certPath">The path to the certificate file.</param>
 584    /// <param name="password">The SecureString password for the certificate, if required.</param>
 585    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 586    /// <param name="flags">Key storage flags for the imported certificate.</param>
 587    /// <returns>The imported X509Certificate2 instance.</returns>
 588    public static X509Certificate2 Import(
 589       string certPath,
 590       SecureString password,
 591       string? privateKeyPath = null,
 592       X509KeyStorageFlags flags = X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.Exportable)
 593    {
 1594        X509Certificate2? result = null;
 1595        Log.Debug("Importing certificate from {CertPath} with flags {Flags}", certPath, flags);
 596        // ToSecureSpan zero-frees its buffer as soon as this callback returns.
 1597        password.ToSecureSpan(span =>
 1598        {
 1599            // capture the return value of the span-based overload
 1600            result = Import(certPath: certPath, password: span, privateKeyPath: privateKeyPath, flags: flags);
 2601        });
 602
 603        // at this point, unmanaged memory is already zeroed
 1604        return result!;   // non-null because the callback always runs exactly once
 605    }
 606
 607    /// <summary>
 608    /// Imports an X509 certificate from the specified file path, with optional private key file and key storage flags.
 609    /// </summary>
 610    /// <param name="certPath">The path to the certificate file.</param>
 611    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 612    /// <param name="flags">Key storage flags for the imported certificate.</param>
 613    /// <returns>The imported X509Certificate2 instance.</returns>
 614    public static X509Certificate2 Import(
 615         string certPath,
 616         string? privateKeyPath = null,
 617         X509KeyStorageFlags flags = X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.Exportable)
 618    {
 619        // ToSecureSpan zero-frees its buffer as soon as this callback returns.
 2620        ReadOnlySpan<char> passwordSpan = default;
 621        // capture the return value of the span-based overload
 2622        var result = Import(certPath: certPath, password: passwordSpan, privateKeyPath: privateKeyPath, flags: flags);
 1623        return result;
 624    }
 625
 626    /// <summary>
 627    /// Imports an X509 certificate from the specified file path.
 628    /// </summary>
 629    /// <param name="certPath">The path to the certificate file.</param>
 630    /// <returns>The imported X509Certificate2 instance.</returns>
 631    public static X509Certificate2 Import(string certPath)
 632    {
 633        // ToSecureSpan zero-frees its buffer as soon as this callback returns.
 4634        ReadOnlySpan<char> passwordSpan = default;
 635        // capture the return value of the span-based overload
 4636        var result = Import(certPath: certPath, password: passwordSpan);
 2637        return result;
 638    }
 639
 640    #endregion
 641
 642    #region Export
 643    /// <summary>
 644    /// Exports the specified X509 certificate to a file in the given format, with optional password and private key inc
 645    /// </summary>
 646    /// <param name="cert">The X509Certificate2 to export.</param>
 647    /// <param name="filePath">The file path to export the certificate to.</param>
 648    /// <param name="fmt">The export format (Pfx or Pem).</param>
 649    /// <param name="password">The password to protect the exported certificate or private key, if applicable.</param>
 650    /// <param name="includePrivateKey">Whether to include the private key in the export.</param>
 651    public static void Export(X509Certificate2 cert, string filePath, ExportFormat fmt,
 652           ReadOnlySpan<char> password = default, bool includePrivateKey = false)
 653    {
 654        // Normalize/validate target path and format
 4655        filePath = NormalizeExportPath(filePath, fmt);
 656
 657        // Ensure output directory exists
 4658        EnsureOutputDirectoryExists(filePath);
 659
 660        // Prepare password shapes once
 4661        using var shapes = CreatePasswordShapes(password);
 662
 663        switch (fmt)
 664        {
 665            case ExportFormat.Pfx:
 2666                ExportPfx(cert, filePath, shapes.Secure);
 2667                break;
 668            case ExportFormat.Pem:
 2669                ExportPem(cert, filePath, password, includePrivateKey);
 2670                break;
 671            default:
 0672                throw new NotSupportedException($"Unsupported export format: {fmt}");
 673        }
 4674    }
 675
 676    /// <summary>
 677    /// Normalizes the export file path based on the desired export format.
 678    /// </summary>
 679    /// <param name="filePath">The original file path.</param>
 680    /// <param name="fmt">The desired export format.</param>
 681    /// <returns>The normalized file path.</returns>
 682    private static string NormalizeExportPath(string filePath, ExportFormat fmt)
 683    {
 4684        var fileExtension = Path.GetExtension(filePath).ToLowerInvariant();
 685        switch (fileExtension)
 686        {
 687            case ".pfx":
 2688                if (fmt != ExportFormat.Pfx)
 689                {
 0690                    throw new NotSupportedException(
 0691                            $"File extension '{fileExtension}' for '{filePath}' is not supported for PFX certificates.")
 692                }
 693
 694                break;
 695            case ".pem":
 2696                if (fmt != ExportFormat.Pem)
 697                {
 0698                    throw new NotSupportedException(
 0699                            $"File extension '{fileExtension}' for '{filePath}' is not supported for PEM certificates.")
 700                }
 701
 702                break;
 703            case "":
 704                // no extension, use the format as the extension
 0705                filePath += fmt == ExportFormat.Pfx ? ".pfx" : ".pem";
 0706                break;
 707            default:
 0708                throw new NotSupportedException(
 0709                    $"File extension '{fileExtension}' for '{filePath}' is not supported. Use .pfx or .pem.");
 710        }
 4711        return filePath;
 712    }
 713
 714    /// <summary>
 715    /// Ensures the output directory exists for the specified file path.
 716    /// </summary>
 717    /// <param name="filePath">The file path to check.</param>
 718    private static void EnsureOutputDirectoryExists(string filePath)
 719    {
 4720        var dir = Path.GetDirectoryName(filePath);
 4721        if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
 722        {
 0723            throw new DirectoryNotFoundException(
 0724                    $"Directory '{dir}' does not exist. Cannot export certificate to {filePath}.");
 725        }
 4726    }
 727
 728    /// <summary>
 729    /// Represents the password shapes used for exporting certificates.
 730    /// </summary>
 4731    private sealed class PasswordShapes(SecureString? secure, char[]? chars) : IDisposable
 732    {
 10733        public SecureString? Secure { get; } = secure;
 14734        public char[]? Chars { get; } = chars;
 735
 736        public void Dispose()
 737        {
 738            try
 739            {
 4740                Secure?.Dispose();
 3741            }
 742            finally
 743            {
 4744                if (Chars is not null)
 745                {
 3746                    Array.Clear(Chars, 0, Chars.Length);
 747                }
 4748            }
 4749        }
 750    }
 751
 752    /// <summary>
 753    /// Creates password shapes from the provided password span.
 754    /// </summary>
 755    /// <param name="password">The password span.</param>
 756    /// <returns>The created password shapes.</returns>
 757    private static PasswordShapes CreatePasswordShapes(ReadOnlySpan<char> password)
 758    {
 4759        var secure = password.IsEmpty ? null : SecureStringUtils.ToSecureString(password);
 4760        var chars = password.IsEmpty ? null : password.ToArray();
 4761        return new PasswordShapes(secure, chars);
 762    }
 763
 764    /// <summary>
 765    /// Exports the specified X509 certificate to a file in the given format.
 766    /// </summary>
 767    /// <param name="cert">The X509Certificate2 to export.</param>
 768    /// <param name="filePath">The file path to export the certificate to.</param>
 769    /// <param name="password">The SecureString password to protect the exported certificate.</param>
 770    private static void ExportPfx(X509Certificate2 cert, string filePath, SecureString? password)
 771    {
 2772        var pfx = cert.Export(X509ContentType.Pfx, password);
 2773        File.WriteAllBytes(filePath, pfx);
 2774    }
 775
 776    /// <summary>
 777    /// Exports the specified X509 certificate to a file in the given format.
 778    /// </summary>
 779    /// <param name="cert">The X509Certificate2 to export.</param>
 780    /// <param name="filePath">The file path to export the certificate to.</param>
 781    /// <param name="password">The SecureString password to protect the exported certificate.</param>
 782    /// <param name="includePrivateKey">Whether to include the private key in the export.</param>
 783    private static void ExportPem(X509Certificate2 cert, string filePath, ReadOnlySpan<char> password, bool includePriva
 784    {
 785        // Write certificate first, then dispose writer before optional key append to avoid file locks on Windows
 2786        using (var sw = new StreamWriter(filePath, false, Encoding.ASCII))
 787        {
 2788            new PemWriter(sw).WriteObject(DotNetUtilities.FromX509Certificate(cert));
 2789        }
 790
 2791        if (includePrivateKey)
 792        {
 1793            WritePrivateKey(cert, password, filePath);
 794            // Fallback safeguard: if append was requested but key block missing, try again
 795            try
 796            {
 1797                if (ShouldAppendKeyToPem && !File.ReadAllText(filePath).Contains("PRIVATE KEY", StringComparison.Ordinal
 798                {
 0799                    var baseName = Path.GetFileNameWithoutExtension(filePath);
 0800                    var dir = Path.GetDirectoryName(filePath);
 0801                    var keyFile = string.IsNullOrEmpty(dir) ? baseName + ".key" : Path.Combine(dir, baseName + ".key");
 0802                    if (File.Exists(keyFile))
 803                    {
 0804                        File.AppendAllText(filePath, Environment.NewLine + File.ReadAllText(keyFile));
 805                    }
 806                }
 1807            }
 0808            catch (Exception ex)
 809            {
 0810                Log.Debug(ex, "Fallback attempt to append private key to PEM failed");
 0811            }
 812        }
 2813    }
 814
 815    /// <summary>
 816    /// Writes the private key of the specified X509 certificate to a file.
 817    /// </summary>
 818    /// <param name="cert">The X509Certificate2 to export.</param>
 819    /// <param name="password">The SecureString password to protect the exported private key.</param>
 820    /// <param name="certFilePath">The file path to export the certificate to.</param>
 821    private static void WritePrivateKey(X509Certificate2 cert, ReadOnlySpan<char> password, string certFilePath)
 822    {
 1823        if (!cert.HasPrivateKey)
 824        {
 0825            throw new InvalidOperationException(
 0826                "Certificate does not contain a private key; cannot export private key PEM.");
 827        }
 828
 829        AsymmetricAlgorithm key;
 830
 831        try
 832        {
 833            // Try RSA first, then ECDSA
 1834            key = (AsymmetricAlgorithm?)cert.GetRSAPrivateKey()
 1835                  ?? cert.GetECDsaPrivateKey()
 1836                  ?? throw new NotSupportedException(
 1837                        "Certificate private key is neither RSA nor ECDSA, or is not accessible.");
 1838        }
 0839        catch (CryptographicException ex) when (ex.HResult == unchecked((int)0x80090016))
 840        {
 841            // 0x80090016 = NTE_BAD_KEYSET  → "Keyset does not exist"
 0842            throw new InvalidOperationException(
 0843                "The certificate reports a private key, but the key container ('keyset') is not accessible. " +
 0844                "This usually means the certificate was loaded without its private key, or the current process " +
 0845                "identity does not have permission to access the key. Re-import the certificate from a PFX " +
 0846                "with the private key and X509KeyStorageFlags.Exportable, or adjust key permissions.",
 0847                ex);
 848        }
 849
 850        byte[] keyDer;
 851        string pemLabel;
 852
 1853        if (password.IsEmpty)
 854        {
 855            // unencrypted PKCS#8
 0856            keyDer = key switch
 0857            {
 0858                RSA rsa => rsa.ExportPkcs8PrivateKey(),
 0859                ECDsa ecc => ecc.ExportPkcs8PrivateKey(),
 0860                _ => throw new NotSupportedException("Only RSA and ECDSA private keys are supported.")
 0861            };
 0862            pemLabel = "PRIVATE KEY";
 863        }
 864        else
 865        {
 866            // encrypted PKCS#8
 1867            var pbe = new PbeParameters(
 1868                PbeEncryptionAlgorithm.Aes256Cbc,
 1869                HashAlgorithmName.SHA256,
 1870                iterationCount: 100_000);
 871
 1872            keyDer = key switch
 1873            {
 1874                RSA rsa => rsa.ExportEncryptedPkcs8PrivateKey(password, pbe),
 0875                ECDsa ecc => ecc.ExportEncryptedPkcs8PrivateKey(password, pbe),
 0876                _ => throw new NotSupportedException("Only RSA and ECDSA private keys are supported.")
 1877            };
 1878            pemLabel = "ENCRYPTED PRIVATE KEY";
 879        }
 880
 1881        var keyPem = PemEncoding.WriteString(pemLabel, keyDer);
 1882        var certDir = Path.GetDirectoryName(certFilePath);
 1883        var baseName = Path.GetFileNameWithoutExtension(certFilePath);
 1884        var keyFilePath = string.IsNullOrEmpty(certDir)
 1885            ? baseName + ".key"
 1886            : Path.Combine(certDir, baseName + ".key");
 887
 1888        File.WriteAllText(keyFilePath, keyPem);
 889
 890        try
 891        {
 1892            if (ShouldAppendKeyToPem)
 893            {
 0894                File.AppendAllText(certFilePath, Environment.NewLine + keyPem);
 895            }
 1896        }
 0897        catch (Exception ex)
 898        {
 0899            Log.Debug(ex,
 0900                "Failed to append private key to certificate PEM file {CertFilePath}; continuing with separate key file 
 0901                certFilePath);
 0902        }
 1903    }
 904
 905    /// <summary>
 906    /// Exports the specified X509 certificate to a file in the given format, using a SecureString password and optional
 907    /// </summary>
 908    /// <param name="cert">The X509Certificate2 to export.</param>
 909    /// <param name="filePath">The file path to export the certificate to.</param>
 910    /// <param name="fmt">The export format (Pfx or Pem).</param>
 911    /// <param name="password">The SecureString password to protect the exported certificate or private key, if applicab
 912    /// <param name="includePrivateKey">Whether to include the private key in the export.</param>
 913    public static void Export(
 914        X509Certificate2 cert,
 915        string filePath,
 916        ExportFormat fmt,
 917        SecureString password,
 918        bool includePrivateKey = false)
 919    {
 1920        if (password is null)
 921        {
 922            // Delegate to span-based overload with no password
 0923            Export(cert, filePath, fmt, [], includePrivateKey);
 924        }
 925        else
 926        {
 1927            password.ToSecureSpan(span =>
 1928                Export(cert, filePath, fmt, span, includePrivateKey)
 1929            // this will run your span‐based implementation,
 1930            // then immediately zero & free the unmanaged buffer
 1931            );
 932        }
 1933    }
 934
 935    /// <summary>
 936    /// Creates a self-signed certificate from the given RSA JWK JSON and exports it
 937    /// as a PEM certificate (optionally including the private key) to the specified path.
 938    /// </summary>
 939    /// <param name="jwkJson">The RSA JWK JSON string.</param>
 940    /// <param name="filePath">
 941    /// Target file path. If no extension is provided, ".pem" will be added.
 942    /// </param>
 943    /// <param name="password">
 944    /// Optional password used to encrypt the private key when <paramref name="includePrivateKey"/> is true.
 945    /// Ignored when <paramref name="includePrivateKey"/> is false.
 946    /// </param>
 947    /// <param name="includePrivateKey">
 948    /// If true, the PEM export will include the private key (and create a .key file as per Export logic).
 949    /// </param>
 950    public static void ExportPemFromJwkJson(
 951        string jwkJson,
 952        string filePath,
 953        ReadOnlySpan<char> password = default,
 954        bool includePrivateKey = false)
 955    {
 0956        if (string.IsNullOrWhiteSpace(jwkJson))
 957        {
 0958            throw new ArgumentException("JWK JSON cannot be null or empty.", nameof(jwkJson));
 959        }
 960
 961        // 1) Create a self-signed certificate from the JWK
 0962        var cert = CreateSelfSignedCertificateFromJwk(jwkJson);
 963
 964        // 2) Reuse the existing Export pipeline to write PEM (cert + optional key)
 0965        Export(cert, filePath, ExportFormat.Pem, password, includePrivateKey);
 0966    }
 967
 968    /// <summary>
 969    /// Creates a self-signed certificate from the given RSA JWK JSON and exports it
 970    /// as a PEM certificate (optionally including the private key) to the specified path,
 971    /// using a <see cref="SecureString"/> password.
 972    /// </summary>
 973    /// <param name="jwkJson">The RSA JWK JSON string.</param>
 974    /// <param name="filePath">Target file path for the PEM output.</param>
 975    /// <param name="password">
 976    /// SecureString password used to encrypt the private key when
 977    /// <paramref name="includePrivateKey"/> is true.
 978    /// </param>
 979    /// <param name="includePrivateKey">
 980    /// If true, the PEM export will include the private key.
 981    /// </param>
 982    public static void ExportPemFromJwkJson(
 983        string jwkJson,
 984        string filePath,
 985        SecureString password,
 986        bool includePrivateKey = false)
 987    {
 0988        if (password is null)
 989        {
 990            // Delegate to span-based overload with no password
 0991            ExportPemFromJwkJson(jwkJson, filePath, [], includePrivateKey);
 0992            return;
 993        }
 994
 0995        password.ToSecureSpan(span =>
 0996        {
 0997            ExportPemFromJwkJson(jwkJson, filePath, span, includePrivateKey);
 0998        });
 0999    }
 1000
 1001    #endregion
 1002
 1003    #region JWK
 1004
 01005    private static readonly JsonSerializerOptions s_jwkJsonOptions = new()
 01006    {
 01007        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 01008        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 01009        WriteIndented = false
 01010    };
 1011
 1012    /// <summary>
 1013    /// Creates a self-signed X509 certificate from the provided RSA JWK JSON string.
 1014    /// </summary>
 1015    /// <param name="jwkJson">The JSON string representing the RSA JWK.</param>
 1016    /// <param name="subjectName">The subject name for the certificate.</param>
 1017    /// <returns>A self-signed X509Certificate2 instance.</returns>
 1018    /// <exception cref="ArgumentException">Thrown when the JWK JSON is invalid.</exception>
 1019    /// <exception cref="NotSupportedException"></exception>
 1020    public static X509Certificate2 CreateSelfSignedCertificateFromJwk(
 1021        string jwkJson,
 1022        string subjectName = "CN=client-jwt")
 1023    {
 01024        var jwk = JsonSerializer.Deserialize<RsaJwk>(jwkJson)
 01025                  ?? throw new ArgumentException("Invalid JWK JSON");
 1026
 01027        if (!string.Equals(jwk.Kty, "RSA", StringComparison.OrdinalIgnoreCase))
 1028        {
 01029            throw new NotSupportedException("Only RSA JWKs are supported.");
 1030        }
 1031
 01032        var rsaParams = new RSAParameters
 01033        {
 01034            Modulus = Base64UrlEncoder.DecodeBytes(jwk.N),
 01035            Exponent = Base64UrlEncoder.DecodeBytes(jwk.E),
 01036            D = Base64UrlEncoder.DecodeBytes(jwk.D),
 01037            P = Base64UrlEncoder.DecodeBytes(jwk.P),
 01038            Q = Base64UrlEncoder.DecodeBytes(jwk.Q),
 01039            DP = Base64UrlEncoder.DecodeBytes(jwk.DP),
 01040            DQ = Base64UrlEncoder.DecodeBytes(jwk.DQ),
 01041            InverseQ = Base64UrlEncoder.DecodeBytes(jwk.QI)
 01042        };
 1043
 01044        using var rsa = RSA.Create();
 01045        rsa.ImportParameters(rsaParams);
 1046
 01047        var req = new CertificateRequest(
 01048            subjectName,
 01049            rsa,
 01050            HashAlgorithmName.SHA256,
 01051            RSASignaturePadding.Pkcs1);
 1052
 1053        // Self-signed, 1 year validity (tune as you like)
 01054        var notBefore = DateTimeOffset.UtcNow.AddDays(-1);
 01055        var notAfter = notBefore.AddYears(1);
 1056
 01057        var cert = req.CreateSelfSigned(notBefore, notAfter);
 1058
 1059        // Export with private key, re-import as X509Certificate2
 01060        var pfxBytes = cert.Export(X509ContentType.Pfx);
 1061#if NET9_0_OR_GREATER
 1062        return X509CertificateLoader.LoadPkcs12(
 1063            pfxBytes,
 1064            password: default,
 1065            keyStorageFlags: X509KeyStorageFlags.Exportable,
 1066            loaderLimits: Pkcs12LoaderLimits.Defaults);
 1067#else
 01068        return new X509Certificate2(pfxBytes, (string?)null,
 01069            X509KeyStorageFlags.Exportable);
 1070#endif
 01071    }
 1072
 1073    /// <summary>
 1074    /// Builds a Private Key JWT for client authentication using the specified certificate.
 1075    /// </summary>
 1076    /// <param name="key">The security key (X509SecurityKey or JsonWebKey) to sign the JWT.</param>
 1077    /// <param name="clientId">The client ID (issuer and subject) for the JWT.</param>
 1078    /// <param name="tokenEndpoint">The token endpoint URL (audience) for the JWT.</param>
 1079    /// <returns>The generated Private Key JWT as a string.</returns>
 1080    public static string BuildPrivateKeyJwt(
 1081        SecurityKey key,
 1082        string clientId,
 1083        string tokenEndpoint)
 1084    {
 01085        var now = DateTimeOffset.UtcNow;
 1086
 01087        var creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
 01088        var handler = new JsonWebTokenHandler();
 1089
 01090        var descriptor = new SecurityTokenDescriptor
 01091        {
 01092            Issuer = clientId,
 01093            Audience = tokenEndpoint,
 01094            Subject = new ClaimsIdentity(
 01095            [
 01096                new Claim("sub", clientId),
 01097                new Claim("jti", Guid.NewGuid().ToString("N"))
 01098            ]),
 01099            NotBefore = now.UtcDateTime,
 01100            IssuedAt = now.UtcDateTime,
 01101            Expires = now.AddMinutes(2).UtcDateTime,
 01102            SigningCredentials = creds
 01103        };
 1104
 01105        return handler.CreateToken(descriptor);
 1106    }
 1107
 1108    /// <summary>
 1109    /// Builds a Private Key JWT for client authentication using the specified X509 certificate.
 1110    /// </summary>
 1111    /// <param name="certificate">The X509 certificate containing the private key.</param>
 1112    /// <param name="clientId">The client ID (issuer and subject) for the JWT.</param>
 1113    /// <param name="tokenEndpoint">The token endpoint URL (audience) for the JWT.</param>
 1114    /// <returns>The generated Private Key JWT as a string.</returns>
 1115    public static string BuildPrivateKeyJwt(
 1116        X509Certificate2 certificate,
 1117        string clientId,
 1118        string tokenEndpoint)
 1119    {
 01120        var key = new X509SecurityKey(certificate)
 01121        {
 01122            KeyId = certificate.Thumbprint
 01123        };
 1124
 01125        return BuildPrivateKeyJwt(key, clientId, tokenEndpoint);
 1126    }
 1127
 1128    /// <summary>
 1129    /// Builds a Private Key JWT for client authentication using the specified JWK JSON string.
 1130    /// </summary>
 1131    /// <param name="jwkJson">The JWK JSON string representing the key.</param>
 1132    /// <param name="clientId">The client ID (issuer and subject) for the JWT.</param>
 1133    /// <param name="tokenEndpoint">The token endpoint URL (audience) for the JWT.</param>
 1134    /// <returns>The generated Private Key JWT as a string.</returns>
 1135    public static string BuildPrivateKeyJwtFromJwkJson(
 1136        string jwkJson,
 1137        string clientId,
 1138        string tokenEndpoint)
 1139    {
 01140        var jwk = new JsonWebKey(jwkJson);
 1141        // You can set KeyId here if you want to use kid from the JSON:
 1142        // jwk.KeyId is automatically populated from "kid" if present.
 1143
 01144        return BuildPrivateKeyJwt(jwk, clientId, tokenEndpoint);
 1145    }
 1146
 1147    /// <summary>
 1148    /// Builds a JWK JSON (RSA) representation of the given certificate.
 1149    /// By default only public parameters are included (safe for publishing as JWKS).
 1150    /// Set <paramref name="includePrivateParameters"/> to true if you want a full private JWK
 1151    /// (for local storage only – never publish it).
 1152    /// </summary>
 1153    /// <param name="certificate">The X509 certificate to convert.</param>
 1154    /// <param name="includePrivateParameters">Whether to include private key parameters in the JWK.</param>
 1155    /// <returns>The JWK JSON string.</returns>
 1156    public static string CreateJwkJsonFromCertificate(
 1157       X509Certificate2 certificate,
 1158       bool includePrivateParameters = false)
 1159    {
 01160        var x509Key = new X509SecurityKey(certificate)
 01161        {
 01162            KeyId = certificate.Thumbprint?.ToLowerInvariant()
 01163        };
 1164
 1165        // Convert to a JsonWebKey (n, e, kid, x5c, etc.)
 01166        var jwk = JsonWebKeyConverter.ConvertFromX509SecurityKey(
 01167            x509Key,
 01168            representAsRsaKey: true);
 1169
 01170        if (!includePrivateParameters)
 1171        {
 1172            // Clean public JWK
 01173            jwk.D = null;
 01174            jwk.P = null;
 01175            jwk.Q = null;
 01176            jwk.DP = null;
 01177            jwk.DQ = null;
 01178            jwk.QI = null;
 1179        }
 1180        else
 1181        {
 01182            if (!certificate.HasPrivateKey)
 1183            {
 01184                throw new InvalidOperationException("Certificate has no private key.");
 1185            }
 1186
 01187            using var rsa = certificate.GetRSAPrivateKey()
 01188                ?? throw new NotSupportedException("Certificate does not contain an RSA private key.");
 1189
 01190            var p = rsa.ExportParameters(true);
 1191
 01192            jwk.N = Base64UrlEncoder.Encode(p.Modulus);
 01193            jwk.E = Base64UrlEncoder.Encode(p.Exponent);
 01194            jwk.D = Base64UrlEncoder.Encode(p.D);
 01195            jwk.P = Base64UrlEncoder.Encode(p.P);
 01196            jwk.Q = Base64UrlEncoder.Encode(p.Q);
 01197            jwk.DP = Base64UrlEncoder.Encode(p.DP);
 01198            jwk.DQ = Base64UrlEncoder.Encode(p.DQ);
 01199            jwk.QI = Base64UrlEncoder.Encode(p.InverseQ);
 1200        }
 1201
 01202        return JsonSerializer.Serialize(jwk, s_jwkJsonOptions);
 1203    }
 1204
 1205    /// <summary>
 1206    /// Creates an RSA JWK JSON from a given RSA instance (must contain private key).
 1207    /// </summary>
 1208    /// <param name="rsa">The RSA instance with a private key.</param>
 1209    /// <param name="keyId">Optional key identifier (kid) to set on the JWK.</param>
 1210    /// <returns>JWK JSON string containing public and private parameters.</returns>
 1211    public static string CreateJwkJsonFromRsa(RSA rsa, string? keyId = null)
 1212    {
 01213        ArgumentNullException.ThrowIfNull(rsa);
 1214
 1215        // true => includes private key params (d, p, q, dp, dq, qi)
 01216        var p = rsa.ExportParameters(includePrivateParameters: true);
 1217
 01218        if (p.D is null || p.P is null || p.Q is null ||
 01219            p.DP is null || p.DQ is null || p.InverseQ is null)
 1220        {
 01221            throw new InvalidOperationException("RSA key does not contain private parameters.");
 1222        }
 1223
 01224        var jwk = new RsaJwk
 01225        {
 01226            Kty = "RSA",
 01227            N = Base64UrlEncoder.Encode(p.Modulus),
 01228            E = Base64UrlEncoder.Encode(p.Exponent),
 01229            D = Base64UrlEncoder.Encode(p.D),
 01230            P = Base64UrlEncoder.Encode(p.P),
 01231            Q = Base64UrlEncoder.Encode(p.Q),
 01232            DP = Base64UrlEncoder.Encode(p.DP),
 01233            DQ = Base64UrlEncoder.Encode(p.DQ),
 01234            QI = Base64UrlEncoder.Encode(p.InverseQ),
 01235            Kid = keyId
 01236        };
 1237
 01238        return JsonSerializer.Serialize(jwk, s_jwkJsonOptions);
 1239    }
 1240
 1241    /// <summary>
 1242    /// Creates an RSA JWK JSON from a PKCS#1 or PKCS#8 RSA private key in PEM format.
 1243    /// </summary>
 1244    /// <param name="rsaPrivateKeyPem">
 1245    /// PEM containing an RSA private key (e.g. "-----BEGIN RSA PRIVATE KEY----- ...").
 1246    /// </param>
 1247    /// <param name="keyId">Optional key identifier (kid) to set on the JWK.</param>
 1248    /// <returns>JWK JSON string containing public and private parameters.</returns>
 1249    public static string CreateJwkJsonFromRsaPrivateKeyPem(
 1250        string rsaPrivateKeyPem,
 1251        string? keyId = null)
 1252    {
 01253        if (string.IsNullOrWhiteSpace(rsaPrivateKeyPem))
 1254        {
 01255            throw new ArgumentException("RSA private key PEM cannot be null or empty.", nameof(rsaPrivateKeyPem));
 1256        }
 1257
 01258        using var rsa = RSA.Create();
 01259        rsa.ImportFromPem(rsaPrivateKeyPem.AsSpan());
 1260
 01261        return CreateJwkJsonFromRsa(rsa, keyId);
 01262    }
 1263
 1264    #endregion
 1265
 1266    #region  Validation helpers (Test-PodeCertificate equivalent)
 1267    /// <summary>
 1268    /// Validates the specified X509 certificate according to the provided options.
 1269    /// </summary>
 1270    /// <param name="cert">The X509Certificate2 to validate.</param>
 1271    /// <param name="checkRevocation">Whether to check certificate revocation status.</param>
 1272    /// <param name="allowWeakAlgorithms">Whether to allow weak algorithms such as SHA-1 or small key sizes.</param>
 1273    /// <param name="denySelfSigned">Whether to deny self-signed certificates.</param>
 1274    /// <param name="expectedPurpose">A collection of expected key purposes (EKU) for the certificate.</param>
 1275    /// <param name="strictPurpose">If true, the certificate must match the expected purposes exactly.</param>
 1276    /// <returns>True if the certificate is valid according to the specified options; otherwise, false.</returns>
 1277    public static bool Validate(
 1278     X509Certificate2 cert,
 1279     bool checkRevocation = false,
 1280     bool allowWeakAlgorithms = false,
 1281     bool denySelfSigned = false,
 1282     OidCollection? expectedPurpose = null,
 1283     bool strictPurpose = false)
 1284    {
 1285        // 1) Validity period
 71286        if (!IsWithinValidityPeriod(cert))
 1287        {
 01288            return false;
 1289        }
 1290
 1291        // 2) Self-signed policy
 71292        var isSelfSigned = cert.Subject == cert.Issuer;
 71293        if (denySelfSigned && isSelfSigned)
 1294        {
 11295            return false;
 1296        }
 1297
 1298        // Pre-compute weakness so we can apply it consistently across validation steps.
 61299        var isWeak = UsesWeakAlgorithms(cert);
 1300
 1301        // 3) Chain build (with optional revocation)
 61302        if (!BuildChainOk(cert, checkRevocation, isSelfSigned, allowWeakAlgorithms, isWeak))
 1303        {
 01304            return false;
 1305        }
 1306
 1307        // 4) EKU / purposes
 61308        if (!PurposesOk(cert, expectedPurpose, strictPurpose))
 1309        {
 11310            return false;
 1311        }
 1312
 1313        // 5) Weak algorithms
 51314        if (!allowWeakAlgorithms && isWeak)
 1315        {
 11316            return false;
 1317        }
 1318
 41319        return true;   // ✅ everything passed
 1320    }
 1321
 1322    /// <summary>
 1323    /// Checks if the certificate is within its validity period.
 1324    /// </summary>
 1325    /// <param name="cert">The X509Certificate2 to check.</param>
 1326    /// <returns>True if the certificate is within its validity period; otherwise, false.</returns>
 1327    private static bool IsWithinValidityPeriod(X509Certificate2 cert)
 1328    {
 71329        var notBeforeUtc = cert.NotBefore.Kind == DateTimeKind.Utc
 71330            ? cert.NotBefore
 71331            : cert.NotBefore.ToUniversalTime();
 1332
 71333        var notAfterUtc = cert.NotAfter.Kind == DateTimeKind.Utc
 71334            ? cert.NotAfter
 71335            : cert.NotAfter.ToUniversalTime();
 1336
 71337        var nowUtc = DateTime.UtcNow;
 71338        return nowUtc >= notBeforeUtc && nowUtc <= notAfterUtc;
 1339    }
 1340
 1341    /// <summary>
 1342    /// Checks if the certificate chain is valid.
 1343    /// </summary>
 1344    /// <param name="cert">The X509Certificate2 to check.</param>
 1345    /// <param name="checkRevocation">Whether to check certificate revocation status.</param>
 1346    /// <param name="isSelfSigned">Whether the certificate is self-signed.</param>
 1347    /// <returns>True if the certificate chain is valid; otherwise, false.</returns>
 1348    private static bool BuildChainOk(X509Certificate2 cert, bool checkRevocation, bool isSelfSigned)
 1349    {
 51350        using var chain = new X509Chain();
 51351        chain.ChainPolicy.RevocationMode = checkRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck;
 51352        chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EndCertificateOnly;
 51353        chain.ChainPolicy.DisableCertificateDownloads = !checkRevocation;
 1354
 51355        if (isSelfSigned)
 1356        {
 1357            // Make self-signed validation deterministic across platforms.
 1358            // Using the platform trust store differs between Windows/macOS/Linux; custom root trust
 1359            // avoids false negatives for dev/self-signed certificates.
 51360            chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
 51361            _ = chain.ChainPolicy.CustomTrustStore.Add(cert);
 51362            chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
 1363        }
 1364
 51365        var ok = chain.Build(cert);
 51366        if (ok)
 1367        {
 51368            return true;
 1369        }
 1370
 01371        if (!isSelfSigned)
 1372        {
 01373            return false;
 1374        }
 1375
 1376        // Some platforms still report non-fatal statuses for self-signed roots.
 1377        // Treat these as acceptable for self-signed certificates.
 01378        var allowed = X509ChainStatusFlags.UntrustedRoot | X509ChainStatusFlags.PartialChain;
 01379        if (!checkRevocation)
 1380        {
 01381            allowed |= X509ChainStatusFlags.RevocationStatusUnknown | X509ChainStatusFlags.OfflineRevocation;
 1382        }
 1383
 01384        var combined = X509ChainStatusFlags.NoError;
 01385        foreach (var status in chain.ChainStatus)
 1386        {
 01387            combined |= status.Status;
 1388        }
 1389
 01390        return (combined & ~allowed) == 0;
 51391    }
 1392
 1393    /// <summary>
 1394    /// Checks if the certificate chain is valid.
 1395    /// </summary>
 1396    /// <param name="cert">The X509Certificate2 to check.</param>
 1397    /// <param name="checkRevocation">Whether to check certificate revocation status.</param>
 1398    /// <param name="isSelfSigned">Whether the certificate is self-signed.</param>
 1399    /// <param name="allowWeakAlgorithms">Whether weak algorithms are allowed.</param>
 1400    /// <param name="isWeak">Whether the certificate is considered weak by this library.</param>
 1401    /// <returns>True if the certificate chain is valid; otherwise, false.</returns>
 1402    private static bool BuildChainOk(
 1403        X509Certificate2 cert,
 1404        bool checkRevocation,
 1405        bool isSelfSigned,
 1406        bool allowWeakAlgorithms,
 61407        bool isWeak) => (isSelfSigned && allowWeakAlgorithms && isWeak) || BuildChainOk(cert, checkRevocation, isSelfSig
 1408
 1409    /// <summary>
 1410    /// Checks if the certificate has the expected key purposes (EKU).
 1411    /// </summary>
 1412    /// <param name="cert">The X509Certificate2 to check.</param>
 1413    /// <param name="expectedPurpose">A collection of expected key purposes (EKU) for the certificate.</param>
 1414    /// <param name="strictPurpose">If true, the certificate must match the expected purposes exactly.</param>
 1415    /// <returns>True if the certificate has the expected purposes; otherwise, false.</returns>
 1416    private static bool PurposesOk(X509Certificate2 cert, OidCollection? expectedPurpose, bool strictPurpose)
 1417    {
 61418        if (expectedPurpose is not { Count: > 0 })
 1419        {
 31420            return true; // nothing to check
 1421        }
 1422
 31423        var eku = GetEkuOids(cert);
 31424        var wanted = expectedPurpose
 31425            .Cast<Oid>()
 41426            .Select(static o => o.Value)
 41427            .Where(static v => !string.IsNullOrWhiteSpace(v))
 41428            .Select(static v => v!)
 31429            .ToHashSet(StringComparer.Ordinal);
 1430
 31431        return wanted.Count == 0 || (eku.Count != 0 && (strictPurpose ? eku.SetEquals(wanted) : wanted.IsSubsetOf(eku)))
 1432    }
 1433
 1434    /// <summary>
 1435    /// Extracts EKU OIDs from the certificate, robustly across platforms.
 1436    /// </summary>
 1437    /// <param name="cert">The certificate to inspect.</param>
 1438    /// <returns>A set of EKU OID strings.</returns>
 1439    private static HashSet<string> GetEkuOids(X509Certificate2 cert)
 1440    {
 31441        var set = new HashSet<string>(StringComparer.Ordinal);
 1442
 1443        // EKU extension OID
 31444        var ext = cert.Extensions["2.5.29.37"];
 31445        if (ext == null)
 1446        {
 01447            return set;
 1448        }
 1449
 31450        var ekuExt = ext as X509EnhancedKeyUsageExtension
 31451            ?? new X509EnhancedKeyUsageExtension(ext, ext.Critical);
 1452
 181453        foreach (var oid in ekuExt.EnhancedKeyUsages.Cast<Oid>())
 1454        {
 61455            if (!string.IsNullOrWhiteSpace(oid.Value))
 1456            {
 61457                _ = set.Add(oid.Value);
 1458            }
 1459        }
 1460
 31461        return set;
 1462    }
 1463
 1464    /// <summary>
 1465    /// Checks if the certificate uses weak algorithms.
 1466    /// </summary>
 1467    /// <param name="cert">The X509Certificate2 to check.</param>
 1468    /// <returns>True if the certificate uses weak algorithms; otherwise, false.</returns>
 1469    private static bool UsesWeakAlgorithms(X509Certificate2 cert)
 1470    {
 61471        var isSha1 = cert.SignatureAlgorithm?.FriendlyName?
 61472                           .Contains("sha1", StringComparison.OrdinalIgnoreCase) == true;
 1473
 61474        var weakRsa = cert.GetRSAPublicKey() is { KeySize: < 2048 };
 61475        var weakDsa = cert.GetDSAPublicKey() is { KeySize: < 2048 };
 61476        var weakEcdsa = cert.GetECDsaPublicKey() is { KeySize: < 256 };  // P-256 minimum
 1477
 61478        return isSha1 || weakRsa || weakDsa || weakEcdsa;
 1479    }
 1480
 1481    /// <summary>
 1482    /// Gets the enhanced key usage purposes (EKU) from the specified X509 certificate.
 1483    /// </summary>
 1484    /// <param name="cert">The X509Certificate2 to extract purposes from.</param>
 1485    /// <returns>An enumerable of purpose names or OID values.</returns>
 1486    public static IEnumerable<string> GetPurposes(X509Certificate2 cert) =>
 11487        cert.Extensions
 11488            .OfType<X509EnhancedKeyUsageExtension>()
 11489            .SelectMany(x => x.EnhancedKeyUsages.Cast<Oid>())
 21490            .Select(o => (o.FriendlyName ?? o.Value)!)   // ← null-forgiving
 31491            .Where(s => s.Length > 0);                   // optional: drop empties
 1492    #endregion
 1493
 1494    #region  private helpers
 1495    private static AsymmetricCipherKeyPair GenRsaKeyPair(int bits, SecureRandom rng)
 1496    {
 131497        var gen = new RsaKeyPairGenerator();
 131498        gen.Init(new KeyGenerationParameters(rng, bits));
 131499        return gen.GenerateKeyPair();
 1500    }
 1501
 1502    /// <summary>
 1503    /// Generates an EC key pair.
 1504    /// </summary>
 1505    /// <param name="bits">The key size in bits.</param>
 1506    /// <param name="rng">The secure random number generator.</param>
 1507    /// <returns>The generated EC key pair.</returns>
 1508    private static AsymmetricCipherKeyPair GenEcKeyPair(int bits, SecureRandom rng)
 1509    {
 1510        // NIST-style names are fine here
 11511        var name = bits switch
 11512        {
 11513            <= 256 => "P-256",
 01514            <= 384 => "P-384",
 01515            _ => "P-521"
 11516        };
 1517
 1518        // ECNamedCurveTable knows about SEC *and* NIST names
 11519        var ecParams = ECNamedCurveTable.GetByName(name)
 11520                       ?? throw new InvalidOperationException($"Curve not found: {name}");
 1521
 11522        var domain = new ECDomainParameters(
 11523            ecParams.Curve, ecParams.G, ecParams.N, ecParams.H, ecParams.GetSeed());
 1524
 11525        var gen = new ECKeyPairGenerator();
 11526        gen.Init(new ECKeyGenerationParameters(domain, rng));
 11527        return gen.GenerateKeyPair();
 1528    }
 1529
 1530    /// <summary>
 1531    /// Converts a BouncyCastle X509Certificate to a .NET X509Certificate2.
 1532    /// </summary>
 1533    /// <param name="cert">The BouncyCastle X509Certificate to convert.</param>
 1534    /// <param name="privKey">The private key associated with the certificate.</param>
 1535    /// <param name="flags">The key storage flags to use.</param>
 1536    /// <param name="ephemeral">Whether the key is ephemeral.</param>
 1537    /// <returns></returns>
 1538    private static X509Certificate2 ToX509Cert2(
 1539        Org.BouncyCastle.X509.X509Certificate cert,
 1540        AsymmetricKeyParameter privKey,
 1541        X509KeyStorageFlags flags,
 1542        bool ephemeral)
 1543    {
 121544        var store = new Pkcs12StoreBuilder().Build();
 121545        var entry = new X509CertificateEntry(cert);
 1546        const string alias = "cert";
 121547        store.SetCertificateEntry(alias, entry);
 121548        store.SetKeyEntry(alias, new AsymmetricKeyEntry(privKey),
 121549                          [entry]);
 1550
 121551        using var ms = new MemoryStream();
 121552        store.Save(ms, [], new SecureRandom());
 121553        var raw = ms.ToArray();
 1554
 1555#if NET9_0_OR_GREATER
 1556        try
 1557        {
 1558            return X509CertificateLoader.LoadPkcs12(
 1559                raw,
 1560                password: default,
 1561                keyStorageFlags: flags | (ephemeral ? X509KeyStorageFlags.EphemeralKeySet : 0),
 1562                loaderLimits: Pkcs12LoaderLimits.Defaults
 1563            );
 1564        }
 1565        catch (PlatformNotSupportedException) when (ephemeral)
 1566        {
 1567            // Some platforms (e.g. certain Linux/macOS runners) don't yet support
 1568            // EphemeralKeySet with the new X509CertificateLoader API. In that case
 1569            // we fall back to re-loading without the EphemeralKeySet flag. The
 1570            // intent of Ephemeral in our API is simply "do not persist in a store" –
 1571            // loading without the flag here still keeps the cert in-memory only.
 1572            Log.Debug("EphemeralKeySet not supported on this platform for X509CertificateLoader; falling back without th
 1573            return X509CertificateLoader.LoadPkcs12(
 1574                raw,
 1575                password: default,
 1576                keyStorageFlags: flags, // omit EphemeralKeySet
 1577                loaderLimits: Pkcs12LoaderLimits.Defaults
 1578            );
 1579        }
 1580#else
 1581        try
 1582        {
 121583            return new X509Certificate2(
 121584                raw,
 121585                (string?)null,
 121586                flags | (ephemeral ? X509KeyStorageFlags.EphemeralKeySet : 0)
 121587            );
 1588        }
 01589        catch (PlatformNotSupportedException) when (ephemeral)
 1590        {
 1591            // macOS (and some Linux distros) under net8 may not support EphemeralKeySet here.
 01592            Log.Debug("EphemeralKeySet not supported on this platform (net8); falling back without the flag.");
 01593            return new X509Certificate2(
 01594                raw,
 01595                (string?)null,
 01596                flags // omit EphemeralKeySet
 01597            );
 1598        }
 1599
 1600#endif
 121601    }
 1602
 1603    #endregion
 1604}

Methods/Properties

get_ShouldAppendKeyToPem()
NewSelfSigned(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)
.cctor()
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)
IsWithinValidityPeriod(System.Security.Cryptography.X509Certificates.X509Certificate2)
BuildChainOk(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Boolean,System.Boolean)
BuildChainOk(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Boolean,System.Boolean,System.Boolean,System.Boolean)
PurposesOk(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Security.Cryptography.OidCollection,System.Boolean)
GetEkuOids(System.Security.Cryptography.X509Certificates.X509Certificate2)
UsesWeakAlgorithms(System.Security.Cryptography.X509Certificates.X509Certificate2)
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)