< 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@0d738bf294e6281b936d031e1979d928007495ff
Line coverage
53%
Covered lines: 300
Uncovered lines: 266
Coverable lines: 566
Total lines: 1529
Line coverage: 53%
Branch coverage
47%
Covered branches: 107
Total branches: 226
Branch coverage: 47.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 08/26/2025 - 14:53:17 Line coverage: 75.5% (291/385) Branch coverage: 56.4% (96/170) Total lines: 1075 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743109/01/2025 - 04:08:24 Line coverage: 74.5% (298/400) Branch coverage: 57.3% (102/178) Total lines: 1122 Tag: Kestrun/Kestrun@d6f26a131219b7a7fcb4e129af3193ec2ec4892909/07/2025 - 18:41:40 Line coverage: 73.5% (311/423) Branch coverage: 57.2% (103/180) Total lines: 1164 Tag: Kestrun/Kestrun@2192d4ccb46312ce89b7f7fda1aa8c915bfa228410/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@7a3839f4de2254e22daae81ab8dc7cb2f40c8330 08/26/2025 - 14:53:17 Line coverage: 75.5% (291/385) Branch coverage: 56.4% (96/170) Total lines: 1075 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743109/01/2025 - 04:08:24 Line coverage: 74.5% (298/400) Branch coverage: 57.3% (102/178) Total lines: 1122 Tag: Kestrun/Kestrun@d6f26a131219b7a7fcb4e129af3193ec2ec4892909/07/2025 - 18:41:40 Line coverage: 73.5% (311/423) Branch coverage: 57.2% (103/180) Total lines: 1164 Tag: Kestrun/Kestrun@2192d4ccb46312ce89b7f7fda1aa8c915bfa228410/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@7a3839f4de2254e22daae81ab8dc7cb2f40c8330

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(...)83.33%131283.33%
IsWithinValidityPeriod(...)50%22100%
BuildChainOk(...)75%44100%
PurposesOk(...)100%66100%
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
 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{
 37    /// <summary>
 38    /// Controls whether the private key is appended to the certificate PEM file in addition to
 39    /// writing a separate .key file. Appending was initially added to work around platform
 40    /// inconsistencies when importing encrypted PEM pairs on some Linux runners. However, having
 41    /// both a combined (cert+key) file and a separate key file can itself introduce ambiguity in
 42    /// which API path <see cref="X509Certificate2"/> chooses (single-file vs dual-file), which was
 43    /// observed to contribute to rare flakiness (private key occasionally not attached after
 44    /// import). To make behavior deterministic we now disable appending by default and allow it to
 45    /// be re-enabled explicitly via the environment variable KESTRUN_APPEND_KEY_TO_PEM.
 46    /// Set KESTRUN_APPEND_KEY_TO_PEM=1 (or "true") to re-enable.
 47    /// </summary>
 48    private static bool ShouldAppendKeyToPem =>
 249        string.Equals(Environment.GetEnvironmentVariable("KESTRUN_APPEND_KEY_TO_PEM"), "1", StringComparison.OrdinalIgno
 250        string.Equals(Environment.GetEnvironmentVariable("KESTRUN_APPEND_KEY_TO_PEM"), "true", StringComparison.OrdinalI
 51
 52    #region  Self-signed certificate
 53    /// <summary>
 54    /// Creates a new self-signed X509 certificate using the specified options.
 55    /// </summary>
 56    /// <param name="o">Options for creating the self-signed certificate.</param>
 57    /// <returns>A new self-signed X509Certificate2 instance.</returns>
 58    public static X509Certificate2 NewSelfSigned(SelfSignedOptions o)
 59    {
 1260        var random = new SecureRandom(new CryptoApiRandomGenerator());
 61
 62        // ── 1. Key pair ───────────────────────────────────────────────────────────
 1263        var keyPair =
 1264            o.KeyType switch
 1265            {
 1266                KeyType.Rsa => GenRsaKeyPair(o.KeyLength, random),
 067                KeyType.Ecdsa => GenEcKeyPair(o.KeyLength, random),
 068                _ => throw new ArgumentOutOfRangeException()
 1269            };
 70
 71        // ── 2. Certificate body ───────────────────────────────────────────────────
 1272        var notBefore = DateTime.UtcNow.AddMinutes(-5);
 1273        var notAfter = notBefore.AddDays(o.ValidDays);
 1274        var serial = BigIntegers.CreateRandomInRange(
 1275                            BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
 76
 1277        var subjectDn = new X509Name($"CN={o.DnsNames.First()}");
 1278        var gen = new X509V3CertificateGenerator();
 1279        gen.SetSerialNumber(serial);
 1280        gen.SetIssuerDN(subjectDn);
 1281        gen.SetSubjectDN(subjectDn);
 1282        gen.SetNotBefore(notBefore);
 1283        gen.SetNotAfter(notAfter);
 1284        gen.SetPublicKey(keyPair.Public);
 85
 86        // SANs
 1287        var altNames = o.DnsNames
 2388                        .Select(n => new GeneralName(
 2389                            IPAddress.TryParse(n, out _) ?
 2390                                GeneralName.IPAddress : GeneralName.DnsName, n))
 1291                        .ToArray();
 1292        gen.AddExtension(X509Extensions.SubjectAlternativeName, false,
 1293                         new DerSequence(altNames));
 94
 95        // EKU
 1296        var eku = o.Purposes ??
 1297         [
 1298             KeyPurposeID.id_kp_serverAuth,
 1299            KeyPurposeID.id_kp_clientAuth
 12100         ];
 12101        gen.AddExtension(X509Extensions.ExtendedKeyUsage, false,
 12102                         new ExtendedKeyUsage([.. eku]));
 103
 104        // KeyUsage – allow digitalSignature & keyEncipherment
 12105        gen.AddExtension(X509Extensions.KeyUsage, true,
 12106                         new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.KeyEncipherment));
 107
 108        // ── 3. Sign & output ──────────────────────────────────────────────────────
 12109        var sigAlg = o.KeyType == KeyType.Rsa ? "SHA256WITHRSA" : "SHA384WITHECDSA";
 12110        var signer = new Asn1SignatureFactory(sigAlg, keyPair.Private, random);
 12111        var cert = gen.Generate(signer);
 112
 12113        return ToX509Cert2(cert, keyPair.Private,
 12114            o.Exportable ? X509KeyStorageFlags.Exportable : X509KeyStorageFlags.DefaultKeySet,
 12115            o.Ephemeral);
 116    }
 117    #endregion
 118
 119    #region  CSR
 120
 121    /// <summary>
 122    /// Creates a new Certificate Signing Request (CSR) and returns the PEM-encoded CSR and the private key.
 123    /// </summary>
 124    /// <param name="options">The options for the CSR.</param>
 125    /// <param name="encryptionPassword">The password to encrypt the private key, if desired.</param>
 126    /// <returns>A <see cref="CsrResult"/> containing the CSR and private key information.</returns>
 127    /// <exception cref="ArgumentOutOfRangeException"></exception>
 128    public static CsrResult NewCertificateRequest(CsrOptions options, ReadOnlySpan<char> encryptionPassword = default)
 129    {
 130        // 0️⃣ Keypair
 2131        var random = new SecureRandom(new CryptoApiRandomGenerator());
 2132        var keyPair = options.KeyType switch
 2133        {
 1134            KeyType.Rsa => GenRsaKeyPair(options.KeyLength, random),
 1135            KeyType.Ecdsa => GenEcKeyPair(options.KeyLength, random),
 0136            _ => throw new ArgumentOutOfRangeException(nameof(options.KeyType))
 2137        };
 138
 139        // 1️⃣ Subject DN
 2140        var order = new List<DerObjectIdentifier>();
 2141        var attrs = new Dictionary<DerObjectIdentifier, string>();
 142        void Add(DerObjectIdentifier oid, string? v)
 143        {
 12144            if (!string.IsNullOrWhiteSpace(v)) { order.Add(oid); attrs[oid] = v; }
 8145        }
 2146        Add(X509Name.C, options.Country);
 2147        Add(X509Name.O, options.Org);
 2148        Add(X509Name.OU, options.OrgUnit);
 2149        Add(X509Name.CN, options.CommonName ?? options.DnsNames.First());
 2150        var subject = new X509Name(order, attrs);
 151
 152        // 2️⃣ SAN extension
 2153        var altNames = options.DnsNames
 3154            .Select(d => new GeneralName(
 3155                IPAddress.TryParse(d, out _)
 3156                    ? GeneralName.IPAddress
 3157                    : GeneralName.DnsName, d))
 2158            .ToArray();
 2159        var sanSeq = new DerSequence(altNames);
 160
 2161        var extGen = new X509ExtensionsGenerator();
 2162        extGen.AddExtension(X509Extensions.SubjectAlternativeName, false, sanSeq);
 2163        var extensions = extGen.Generate();
 164
 2165        var extensionRequestAttr = new AttributePkcs(
 2166            PkcsObjectIdentifiers.Pkcs9AtExtensionRequest,
 2167            new DerSet(extensions));
 2168        var attrSet = new DerSet(extensionRequestAttr);
 169
 170        // 3️⃣ CSR
 2171        var sigAlg = options.KeyType == KeyType.Rsa ? "SHA256WITHRSA" : "SHA384WITHECDSA";
 2172        var csr = new Pkcs10CertificationRequest(sigAlg, subject, keyPair.Public, attrSet, keyPair.Private);
 173
 174        // 4️⃣ CSR PEM + DER
 175        string csrPem;
 2176        using (var sw = new StringWriter())
 177        {
 2178            new PemWriter(sw).WriteObject(csr);
 2179            csrPem = sw.ToString();
 2180        }
 2181        var csrDer = csr.GetEncoded();
 182
 183        // 5️⃣ Private key PEM + DER
 184        string privateKeyPem;
 2185        using (var sw = new StringWriter())
 186        {
 2187            new PemWriter(sw).WriteObject(keyPair.Private);
 2188            privateKeyPem = sw.ToString();
 2189        }
 2190        var pkInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(keyPair.Private);
 2191        var privateKeyDer = pkInfo.GetEncoded();
 192
 193        // 6️⃣ Optional encrypted PEM
 2194        string? privateKeyPemEncrypted = null;
 2195        if (!encryptionPassword.IsEmpty)
 196        {
 0197            var pwd = encryptionPassword.ToArray(); // BC requires char[]
 198            try
 199            {
 0200                var gen = new Pkcs8Generator(keyPair.Private, Pkcs8Generator.PbeSha1_3DES)
 0201                {
 0202                    Password = pwd
 0203                };
 0204                using var encSw = new StringWriter();
 0205                new PemWriter(encSw).WriteObject(gen);
 0206                privateKeyPemEncrypted = encSw.ToString();
 207            }
 208            finally
 209            {
 0210                Array.Clear(pwd, 0, pwd.Length); // wipe memory
 0211            }
 212        }
 213
 214        // 7️⃣ Public key PEM + DER
 2215        var spki = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(keyPair.Public);
 2216        var publicKeyDer = spki.GetEncoded();
 217        string publicKeyPem;
 2218        using (var sw = new StringWriter())
 219        {
 2220            new PemWriter(sw).WriteObject(spki);
 2221            publicKeyPem = sw.ToString();
 2222        }
 223
 2224        return new CsrResult(
 2225            csrPem,
 2226            csrDer,
 2227            keyPair.Private,
 2228            privateKeyPem,
 2229            privateKeyDer,
 2230            privateKeyPemEncrypted,
 2231            publicKeyPem,
 2232            publicKeyDer
 2233        );
 234    }
 235
 236
 237    #endregion
 238
 239    #region  Import
 240    /// <summary>
 241    /// Imports an X509 certificate from the specified file path, with optional password and private key file.
 242    /// </summary>
 243    /// <param name="certPath">The path to the certificate file.</param>
 244    /// <param name="password">The password for the certificate, if required.</param>
 245    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 246    /// <param name="flags">Key storage flags for the imported certificate.</param>
 247    /// <returns>The imported X509Certificate2 instance.</returns>
 248    public static X509Certificate2 Import(
 249       string certPath,
 250       ReadOnlySpan<char> password = default,
 251       string? privateKeyPath = null,
 252       X509KeyStorageFlags flags = X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.Exportable)
 253    {
 9254        ValidateImportInputs(certPath, privateKeyPath);
 255
 6256        var ext = Path.GetExtension(certPath).ToLowerInvariant();
 6257        return ext switch
 6258        {
 2259            ".pfx" or ".p12" => ImportPfx(certPath, password, flags),
 1260            ".cer" or ".der" => ImportDer(certPath),
 3261            ".pem" or ".crt" => ImportPem(certPath, password, privateKeyPath),
 0262            _ => throw new NotSupportedException($"Certificate extension '{ext}' is not supported.")
 6263        };
 264    }
 265
 266    /// <summary>
 267    /// Validates the inputs for importing a certificate.
 268    /// </summary>
 269    /// <param name="certPath">The path to the certificate file.</param>
 270    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 271    private static void ValidateImportInputs(string certPath, string? privateKeyPath)
 272    {
 9273        if (string.IsNullOrEmpty(certPath))
 274        {
 1275            throw new ArgumentException("Certificate path cannot be null or empty.", nameof(certPath));
 276        }
 8277        if (!File.Exists(certPath))
 278        {
 1279            throw new FileNotFoundException("Certificate file not found.", certPath);
 280        }
 7281        if (!string.IsNullOrEmpty(privateKeyPath) && !File.Exists(privateKeyPath))
 282        {
 1283            throw new FileNotFoundException("Private key file not found.", privateKeyPath);
 284        }
 6285    }
 286
 287    /// <summary>
 288    /// Imports a PFX certificate from the specified file path.
 289    /// </summary>
 290    /// <param name="certPath">The path to the certificate file.</param>
 291    /// <param name="password">The password for the certificate, if required.</param>
 292    /// <param name="flags">Key storage flags for the imported certificate.</param>
 293    /// <returns>The imported X509Certificate2 instance.</returns>
 294    private static X509Certificate2 ImportPfx(string certPath, ReadOnlySpan<char> password, X509KeyStorageFlags flags)
 295#if NET9_0_OR_GREATER
 296        => X509CertificateLoader.LoadPkcs12FromFile(certPath, password, flags, Pkcs12LoaderLimits.Defaults);
 297#else
 2298        => new(File.ReadAllBytes(certPath), password, flags);
 299#endif
 300
 301    private static X509Certificate2 ImportDer(string certPath)
 302#if NET9_0_OR_GREATER
 303        => X509CertificateLoader.LoadCertificateFromFile(certPath);
 304#else
 1305        => new(File.ReadAllBytes(certPath));
 306#endif
 307
 308
 309    /// <summary>
 310    /// Imports a PEM certificate from the specified file path.
 311    /// </summary>
 312    /// <param name="certPath">The path to the certificate file.</param>
 313    /// <param name="password">The password for the certificate, if required.</param>
 314    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 315    /// <returns>The imported X509Certificate2 instance.</returns>
 316    private static X509Certificate2 ImportPem(string certPath, ReadOnlySpan<char> password, string? privateKeyPath)
 317    {
 318        // No separate key file provided
 3319        if (string.IsNullOrEmpty(privateKeyPath))
 320        {
 1321            return password.IsEmpty
 1322                ? LoadCertOnlyPem(certPath)
 1323                : X509Certificate2.CreateFromEncryptedPemFile(certPath, password);
 324        }
 325
 326        // Separate key file provided
 2327        return password.IsEmpty
 2328            ? ImportPemUnencrypted(certPath, privateKeyPath)
 2329            : ImportPemEncrypted(certPath, password, privateKeyPath);
 330    }
 331
 332    /// <summary>
 333    /// Imports an unencrypted PEM certificate from the specified file path.
 334    /// </summary>
 335    /// <param name="certPath">The path to the certificate file.</param>
 336    /// <param name="privateKeyPath">The path to the private key file.</param>
 337    /// <returns>The imported X509Certificate2 instance.</returns>
 338    private static X509Certificate2 ImportPemUnencrypted(string certPath, string privateKeyPath)
 1339        => X509Certificate2.CreateFromPemFile(certPath, privateKeyPath);
 340
 341    /// <summary>
 342    /// Imports a PEM certificate from the specified file path.
 343    /// </summary>
 344    /// <param name="certPath">The path to the certificate file.</param>
 345    /// <param name="password">The password for the certificate, if required.</param>
 346    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 347    /// <returns>The imported X509Certificate2 instance.</returns>
 348    private static X509Certificate2 ImportPemEncrypted(string certPath, ReadOnlySpan<char> password, string privateKeyPa
 349    {
 350        // Prefer single-file path (combined) first for reliability on some platforms
 351        try
 352        {
 1353            var single = X509Certificate2.CreateFromEncryptedPemFile(certPath, password);
 0354            if (single.HasPrivateKey)
 355            {
 0356                Log.Debug("Imported encrypted PEM using single-file path (combined cert+key) for {CertPath}", certPath);
 0357                return single;
 358            }
 0359        }
 1360        catch (Exception exSingle)
 361        {
 1362            Log.Debug(exSingle, "Single-file encrypted PEM import failed, falling back to separate key file {KeyFile}", 
 1363        }
 364
 1365        var loaded = X509Certificate2.CreateFromEncryptedPemFile(certPath, password, privateKeyPath);
 366
 1367        if (loaded.HasPrivateKey)
 368        {
 1369            return loaded;
 370        }
 371
 372        // Fallback manual pairing if platform failed to associate the key
 0373        TryManualEncryptedPemPairing(certPath, password, privateKeyPath, ref loaded);
 0374        return loaded;
 0375    }
 376
 377    /// <summary>
 378    /// Tries to manually pair an encrypted PEM certificate with its private key.
 379    /// </summary>
 380    /// <param name="certPath">The path to the certificate file.</param>
 381    /// <param name="password">The password for the certificate, if required.</param>
 382    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 383    /// <param name="loaded">The loaded X509Certificate2 instance.</param>
 384    private static void TryManualEncryptedPemPairing(string certPath, ReadOnlySpan<char> password, string privateKeyPath
 385    {
 386        try
 387        {
 0388            var certOnly = LoadCertOnlyPem(certPath);
 0389            var encDer = ExtractEncryptedPemDer(privateKeyPath);
 390
 0391            if (encDer is null)
 392            {
 0393                Log.Debug("Encrypted PEM manual pairing fallback skipped: markers not found in key file {KeyFile}", priv
 0394                return;
 395            }
 396
 0397            var lastErr = TryPairCertificateWithKey(certOnly, password, encDer, ref loaded);
 398
 0399            if (lastErr != null)
 400            {
 0401                Log.Debug(lastErr, "Encrypted PEM manual pairing attempts failed (all rounds); returning original loaded
 402            }
 0403        }
 0404        catch (Exception ex)
 405        {
 0406            Log.Debug(ex, "Encrypted PEM manual pairing fallback failed unexpectedly; returning original loaded certific
 0407        }
 0408    }
 409
 410    /// <summary>
 411    /// Extracts the encrypted PEM DER bytes from a private key file.
 412    /// </summary>
 413    /// <param name="privateKeyPath">The path to the private key file.</param>
 414    /// <returns>The DER bytes if successful, null otherwise.</returns>
 415    private static byte[]? ExtractEncryptedPemDer(string privateKeyPath)
 416    {
 417        const string encBegin = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
 418        const string encEnd = "-----END ENCRYPTED PRIVATE KEY-----";
 419
 0420        byte[]? encDer = null;
 0421        for (var attempt = 0; attempt < 5 && encDer is null; attempt++)
 422        {
 0423            var keyPem = File.ReadAllText(privateKeyPath);
 0424            var start = keyPem.IndexOf(encBegin, StringComparison.Ordinal);
 0425            var end = keyPem.IndexOf(encEnd, StringComparison.Ordinal);
 0426            if (start >= 0 && end > start)
 427            {
 0428                start += encBegin.Length;
 0429                var b64 = keyPem[start..end].Replace("\r", "").Replace("\n", "").Trim();
 0430                try { encDer = Convert.FromBase64String(b64); }
 0431                catch (FormatException fe)
 432                {
 0433                    Log.Debug(fe, "Base64 decode failed on attempt {Attempt} reading encrypted key; retrying", attempt +
 0434                }
 435            }
 0436            if (encDer is null)
 437            {
 0438                Thread.Sleep(40 * (attempt + 1));
 439            }
 440        }
 441
 0442        return encDer;
 443    }
 444
 445    /// <summary>
 446    /// Attempts to pair a certificate with an encrypted private key using RSA and ECDSA.
 447    /// </summary>
 448    /// <param name="certOnly">The certificate without a private key.</param>
 449    /// <param name="password">The password for the encrypted key.</param>
 450    /// <param name="encDer">The encrypted DER bytes.</param>
 451    /// <param name="loaded">The loaded certificate (updated if pairing succeeds).</param>
 452    /// <returns>The last exception encountered, or null if pairing succeeded.</returns>
 453    private static Exception? TryPairCertificateWithKey(X509Certificate2 certOnly, ReadOnlySpan<char> password, byte[] e
 454    {
 0455        Exception? lastErr = null;
 0456        for (var round = 0; round < 2; round++)
 457        {
 0458            if (TryPairWithRsa(certOnly, password, encDer, round, ref loaded, ref lastErr))
 459            {
 0460                return null;
 461            }
 462
 0463            if (TryPairWithEcdsa(certOnly, password, encDer, round, ref loaded, ref lastErr))
 464            {
 0465                return null;
 466            }
 467
 0468            Thread.Sleep(25 * (round + 1));
 469        }
 0470        return lastErr;
 471    }
 472
 473    /// <summary>
 474    /// Tries to pair a certificate with an RSA private key.
 475    /// </summary>
 476    /// <param name="certOnly">The certificate without a private key.</param>
 477    /// <param name="password">The password for the encrypted key.</param>
 478    /// <param name="encDer">The encrypted DER bytes.</param>
 479    /// <param name="round">The attempt round number.</param>
 480    /// <param name="loaded">The loaded certificate (updated if pairing succeeds).</param>
 481    /// <param name="lastErr">The last exception encountered (updated on failure).</param>
 482    /// <returns>True if pairing succeeded, false otherwise.</returns>
 483    private static bool TryPairWithRsa(X509Certificate2 certOnly, ReadOnlySpan<char> password, byte[] encDer, int round,
 484    {
 485        try
 486        {
 0487            using var rsa = RSA.Create();
 0488            rsa.ImportEncryptedPkcs8PrivateKey(password, encDer, out _);
 0489            var withKey = certOnly.CopyWithPrivateKey(rsa);
 0490            if (withKey.HasPrivateKey)
 491            {
 0492                Log.Debug("Encrypted PEM manual pairing succeeded with RSA private key (round {Round}).", round + 1);
 0493                loaded = withKey;
 0494                return true;
 495            }
 0496        }
 0497        catch (Exception exRsa)
 498        {
 0499            lastErr = lastErr is null ? exRsa : new AggregateException(lastErr, exRsa);
 0500        }
 0501        return false;
 0502    }
 503
 504    /// <summary>
 505    /// Tries to pair a certificate with an ECDSA private key.
 506    /// </summary>
 507    /// <param name="certOnly">The certificate without a private key.</param>
 508    /// <param name="password">The password for the encrypted key.</param>
 509    /// <param name="encDer">The encrypted DER bytes.</param>
 510    /// <param name="round">The attempt round number.</param>
 511    /// <param name="loaded">The loaded certificate (updated if pairing succeeds).</param>
 512    /// <param name="lastErr">The last exception encountered (updated on failure).</param>
 513    /// <returns>True if pairing succeeded, false otherwise.</returns>
 514    private static bool TryPairWithEcdsa(X509Certificate2 certOnly, ReadOnlySpan<char> password, byte[] encDer, int roun
 515    {
 516        try
 517        {
 0518            using var ecdsa = ECDsa.Create();
 0519            ecdsa.ImportEncryptedPkcs8PrivateKey(password, encDer, out _);
 0520            var withKey = certOnly.CopyWithPrivateKey(ecdsa);
 0521            if (withKey.HasPrivateKey)
 522            {
 0523                Log.Debug("Encrypted PEM manual pairing succeeded with ECDSA private key (round {Round}).", round + 1);
 0524                loaded = withKey;
 0525                return true;
 526            }
 0527        }
 0528        catch (Exception exEc)
 529        {
 0530            lastErr = lastErr is null ? exEc : new AggregateException(lastErr, exEc);
 0531        }
 0532        return false;
 0533    }
 534
 535    /// <summary>
 536    /// Loads a certificate from a PEM file that contains *only* a CERTIFICATE block (no key).
 537    /// </summary>
 538    /// <param name="certPath">The path to the certificate file.</param>
 539    /// <returns>The loaded X509Certificate2 instance.</returns>
 540    private static X509Certificate2 LoadCertOnlyPem(string certPath)
 541    {
 542        // 1) Read + trim the whole PEM text
 1543        var pem = File.ReadAllText(certPath).Trim();
 544
 545        // 2) Define the BEGIN/END markers
 546        const string begin = "-----BEGIN CERTIFICATE-----";
 547        const string end = "-----END CERTIFICATE-----";
 548
 549        // 3) Find their positions
 1550        var start = pem.IndexOf(begin, StringComparison.Ordinal);
 1551        if (start < 0)
 552        {
 0553            throw new InvalidDataException("BEGIN CERTIFICATE marker not found");
 554        }
 555
 1556        start += begin.Length;
 557
 1558        var stop = pem.IndexOf(end, start, StringComparison.Ordinal);
 1559        if (stop < 0)
 560        {
 0561            throw new InvalidDataException("END CERTIFICATE marker not found");
 562        }
 563
 564        // 4) Extract, clean, and decode the Base64 payload
 1565        var b64 = pem[start..stop]
 1566                       .Replace("\r", "")
 1567                       .Replace("\n", "")
 1568                       .Trim();
 1569        var der = Convert.FromBase64String(b64);
 570
 571        // 5) Return the X509Certificate2
 572
 573#if NET9_0_OR_GREATER
 574        return X509CertificateLoader.LoadCertificate(der);
 575#else
 576        // .NET 8 or earlier path, using X509Certificate2 ctor
 577        // Note: this will not work in .NET 9+ due to the new X509CertificateLoader API
 578        //       which requires a byte array or a file path.
 1579        return new X509Certificate2(der);
 580#endif
 581    }
 582
 583    /// <summary>
 584    /// Imports an X509 certificate from the specified file path, using a SecureString password and optional private key
 585    /// </summary>
 586    /// <param name="certPath">The path to the certificate file.</param>
 587    /// <param name="password">The SecureString password for the certificate, if required.</param>
 588    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 589    /// <param name="flags">Key storage flags for the imported certificate.</param>
 590    /// <returns>The imported X509Certificate2 instance.</returns>
 591    public static X509Certificate2 Import(
 592       string certPath,
 593       SecureString password,
 594       string? privateKeyPath = null,
 595       X509KeyStorageFlags flags = X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.Exportable)
 596    {
 1597        X509Certificate2? result = null;
 1598        Log.Debug("Importing certificate from {CertPath} with flags {Flags}", certPath, flags);
 599        // ToSecureSpan zero-frees its buffer as soon as this callback returns.
 1600        password.ToSecureSpan(span =>
 1601        {
 1602            // capture the return value of the span-based overload
 1603            result = Import(certPath: certPath, password: span, privateKeyPath: privateKeyPath, flags: flags);
 2604        });
 605
 606        // at this point, unmanaged memory is already zeroed
 1607        return result!;   // non-null because the callback always runs exactly once
 608    }
 609
 610    /// <summary>
 611    /// Imports an X509 certificate from the specified file path, with optional private key file and key storage flags.
 612    /// </summary>
 613    /// <param name="certPath">The path to the certificate file.</param>
 614    /// <param name="privateKeyPath">The path to the private key file, if separate.</param>
 615    /// <param name="flags">Key storage flags for the imported certificate.</param>
 616    /// <returns>The imported X509Certificate2 instance.</returns>
 617    public static X509Certificate2 Import(
 618         string certPath,
 619         string? privateKeyPath = null,
 620         X509KeyStorageFlags flags = X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.Exportable)
 621    {
 622        // ToSecureSpan zero-frees its buffer as soon as this callback returns.
 2623        ReadOnlySpan<char> passwordSpan = default;
 624        // capture the return value of the span-based overload
 2625        var result = Import(certPath: certPath, password: passwordSpan, privateKeyPath: privateKeyPath, flags: flags);
 1626        return result;
 627    }
 628
 629    /// <summary>
 630    /// Imports an X509 certificate from the specified file path.
 631    /// </summary>
 632    /// <param name="certPath">The path to the certificate file.</param>
 633    /// <returns>The imported X509Certificate2 instance.</returns>
 634    public static X509Certificate2 Import(string certPath)
 635    {
 636        // ToSecureSpan zero-frees its buffer as soon as this callback returns.
 4637        ReadOnlySpan<char> passwordSpan = default;
 638        // capture the return value of the span-based overload
 4639        var result = Import(certPath: certPath, password: passwordSpan);
 2640        return result;
 641    }
 642
 643
 644
 645    #endregion
 646
 647    #region Export
 648    /// <summary>
 649    /// Exports the specified X509 certificate to a file in the given format, with optional password and private key inc
 650    /// </summary>
 651    /// <param name="cert">The X509Certificate2 to export.</param>
 652    /// <param name="filePath">The file path to export the certificate to.</param>
 653    /// <param name="fmt">The export format (Pfx or Pem).</param>
 654    /// <param name="password">The password to protect the exported certificate or private key, if applicable.</param>
 655    /// <param name="includePrivateKey">Whether to include the private key in the export.</param>
 656    public static void Export(X509Certificate2 cert, string filePath, ExportFormat fmt,
 657           ReadOnlySpan<char> password = default, bool includePrivateKey = false)
 658    {
 659        // Normalize/validate target path and format
 4660        filePath = NormalizeExportPath(filePath, fmt);
 661
 662        // Ensure output directory exists
 4663        EnsureOutputDirectoryExists(filePath);
 664
 665        // Prepare password shapes once
 4666        using var shapes = CreatePasswordShapes(password);
 667
 668        switch (fmt)
 669        {
 670            case ExportFormat.Pfx:
 2671                ExportPfx(cert, filePath, shapes.Secure);
 2672                break;
 673            case ExportFormat.Pem:
 2674                ExportPem(cert, filePath, password, includePrivateKey);
 2675                break;
 676            default:
 0677                throw new NotSupportedException($"Unsupported export format: {fmt}");
 678        }
 4679    }
 680
 681    /// <summary>
 682    /// Normalizes the export file path based on the desired export format.
 683    /// </summary>
 684    /// <param name="filePath">The original file path.</param>
 685    /// <param name="fmt">The desired export format.</param>
 686    /// <returns>The normalized file path.</returns>
 687    private static string NormalizeExportPath(string filePath, ExportFormat fmt)
 688    {
 4689        var fileExtension = Path.GetExtension(filePath).ToLowerInvariant();
 690        switch (fileExtension)
 691        {
 692            case ".pfx":
 2693                if (fmt != ExportFormat.Pfx)
 694                {
 0695                    throw new NotSupportedException(
 0696                            $"File extension '{fileExtension}' for '{filePath}' is not supported for PFX certificates.")
 697                }
 698
 699                break;
 700            case ".pem":
 2701                if (fmt != ExportFormat.Pem)
 702                {
 0703                    throw new NotSupportedException(
 0704                            $"File extension '{fileExtension}' for '{filePath}' is not supported for PEM certificates.")
 705                }
 706
 707                break;
 708            case "":
 709                // no extension, use the format as the extension
 0710                filePath += fmt == ExportFormat.Pfx ? ".pfx" : ".pem";
 0711                break;
 712            default:
 0713                throw new NotSupportedException(
 0714                    $"File extension '{fileExtension}' for '{filePath}' is not supported. Use .pfx or .pem.");
 715        }
 4716        return filePath;
 717    }
 718
 719    /// <summary>
 720    /// Ensures the output directory exists for the specified file path.
 721    /// </summary>
 722    /// <param name="filePath">The file path to check.</param>
 723    private static void EnsureOutputDirectoryExists(string filePath)
 724    {
 4725        var dir = Path.GetDirectoryName(filePath);
 4726        if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
 727        {
 0728            throw new DirectoryNotFoundException(
 0729                    $"Directory '{dir}' does not exist. Cannot export certificate to {filePath}.");
 730        }
 4731    }
 732
 733    /// <summary>
 734    /// Represents the password shapes used for exporting certificates.
 735    /// </summary>
 4736    private sealed class PasswordShapes(SecureString? secure, char[]? chars) : IDisposable
 737    {
 10738        public SecureString? Secure { get; } = secure;
 14739        public char[]? Chars { get; } = chars;
 740
 741        public void Dispose()
 742        {
 743            try
 744            {
 4745                Secure?.Dispose();
 3746            }
 747            finally
 748            {
 4749                if (Chars is not null)
 750                {
 3751                    Array.Clear(Chars, 0, Chars.Length);
 752                }
 4753            }
 4754        }
 755    }
 756
 757    /// <summary>
 758    /// Creates password shapes from the provided password span.
 759    /// </summary>
 760    /// <param name="password">The password span.</param>
 761    /// <returns>The created password shapes.</returns>
 762    private static PasswordShapes CreatePasswordShapes(ReadOnlySpan<char> password)
 763    {
 4764        var secure = password.IsEmpty ? null : SecureStringUtils.ToSecureString(password);
 4765        var chars = password.IsEmpty ? null : password.ToArray();
 4766        return new PasswordShapes(secure, chars);
 767    }
 768
 769    /// <summary>
 770    /// Exports the specified X509 certificate to a file in the given format.
 771    /// </summary>
 772    /// <param name="cert">The X509Certificate2 to export.</param>
 773    /// <param name="filePath">The file path to export the certificate to.</param>
 774    /// <param name="password">The SecureString password to protect the exported certificate.</param>
 775    private static void ExportPfx(X509Certificate2 cert, string filePath, SecureString? password)
 776    {
 2777        var pfx = cert.Export(X509ContentType.Pfx, password);
 2778        File.WriteAllBytes(filePath, pfx);
 2779    }
 780
 781    /// <summary>
 782    /// Exports the specified X509 certificate to a file in the given format.
 783    /// </summary>
 784    /// <param name="cert">The X509Certificate2 to export.</param>
 785    /// <param name="filePath">The file path to export the certificate to.</param>
 786    /// <param name="password">The SecureString password to protect the exported certificate.</param>
 787    /// <param name="includePrivateKey">Whether to include the private key in the export.</param>
 788    private static void ExportPem(X509Certificate2 cert, string filePath, ReadOnlySpan<char> password, bool includePriva
 789    {
 790        // Write certificate first, then dispose writer before optional key append to avoid file locks on Windows
 2791        using (var sw = new StreamWriter(filePath, false, Encoding.ASCII))
 792        {
 2793            new PemWriter(sw).WriteObject(DotNetUtilities.FromX509Certificate(cert));
 2794        }
 795
 2796        if (includePrivateKey)
 797        {
 1798            WritePrivateKey(cert, password, filePath);
 799            // Fallback safeguard: if append was requested but key block missing, try again
 800            try
 801            {
 1802                if (ShouldAppendKeyToPem && !File.ReadAllText(filePath).Contains("PRIVATE KEY", StringComparison.Ordinal
 803                {
 0804                    var baseName = Path.GetFileNameWithoutExtension(filePath);
 0805                    var dir = Path.GetDirectoryName(filePath);
 0806                    var keyFile = string.IsNullOrEmpty(dir) ? baseName + ".key" : Path.Combine(dir, baseName + ".key");
 0807                    if (File.Exists(keyFile))
 808                    {
 0809                        File.AppendAllText(filePath, Environment.NewLine + File.ReadAllText(keyFile));
 810                    }
 811                }
 1812            }
 0813            catch (Exception ex)
 814            {
 0815                Log.Debug(ex, "Fallback attempt to append private key to PEM failed");
 0816            }
 817        }
 2818    }
 819
 820    /// <summary>
 821    /// Writes the private key of the specified X509 certificate to a file.
 822    /// </summary>
 823    /// <param name="cert">The X509Certificate2 to export.</param>
 824    /// <param name="password">The SecureString password to protect the exported private key.</param>
 825    /// <param name="certFilePath">The file path to export the certificate to.</param>
 826    private static void WritePrivateKey(X509Certificate2 cert, ReadOnlySpan<char> password, string certFilePath)
 827    {
 1828        if (!cert.HasPrivateKey)
 829        {
 0830            throw new InvalidOperationException(
 0831                "Certificate does not contain a private key; cannot export private key PEM.");
 832        }
 833
 834        AsymmetricAlgorithm key;
 835
 836        try
 837        {
 838            // Try RSA first, then ECDSA
 1839            key = (AsymmetricAlgorithm?)cert.GetRSAPrivateKey()
 1840                  ?? cert.GetECDsaPrivateKey()
 1841                  ?? throw new NotSupportedException(
 1842                        "Certificate private key is neither RSA nor ECDSA, or is not accessible.");
 1843        }
 0844        catch (CryptographicException ex) when (ex.HResult == unchecked((int)0x80090016))
 845        {
 846            // 0x80090016 = NTE_BAD_KEYSET  → "Keyset does not exist"
 0847            throw new InvalidOperationException(
 0848                "The certificate reports a private key, but the key container ('keyset') is not accessible. " +
 0849                "This usually means the certificate was loaded without its private key, or the current process " +
 0850                "identity does not have permission to access the key. Re-import the certificate from a PFX " +
 0851                "with the private key and X509KeyStorageFlags.Exportable, or adjust key permissions.",
 0852                ex);
 853        }
 854
 855        byte[] keyDer;
 856        string pemLabel;
 857
 1858        if (password.IsEmpty)
 859        {
 860            // unencrypted PKCS#8
 0861            keyDer = key switch
 0862            {
 0863                RSA rsa => rsa.ExportPkcs8PrivateKey(),
 0864                ECDsa ecc => ecc.ExportPkcs8PrivateKey(),
 0865                _ => throw new NotSupportedException("Only RSA and ECDSA private keys are supported.")
 0866            };
 0867            pemLabel = "PRIVATE KEY";
 868        }
 869        else
 870        {
 871            // encrypted PKCS#8
 1872            var pbe = new PbeParameters(
 1873                PbeEncryptionAlgorithm.Aes256Cbc,
 1874                HashAlgorithmName.SHA256,
 1875                iterationCount: 100_000);
 876
 1877            keyDer = key switch
 1878            {
 1879                RSA rsa => rsa.ExportEncryptedPkcs8PrivateKey(password, pbe),
 0880                ECDsa ecc => ecc.ExportEncryptedPkcs8PrivateKey(password, pbe),
 0881                _ => throw new NotSupportedException("Only RSA and ECDSA private keys are supported.")
 1882            };
 1883            pemLabel = "ENCRYPTED PRIVATE KEY";
 884        }
 885
 1886        var keyPem = PemEncoding.WriteString(pemLabel, keyDer);
 1887        var certDir = Path.GetDirectoryName(certFilePath);
 1888        var baseName = Path.GetFileNameWithoutExtension(certFilePath);
 1889        var keyFilePath = string.IsNullOrEmpty(certDir)
 1890            ? baseName + ".key"
 1891            : Path.Combine(certDir, baseName + ".key");
 892
 1893        File.WriteAllText(keyFilePath, keyPem);
 894
 895        try
 896        {
 1897            if (ShouldAppendKeyToPem)
 898            {
 0899                File.AppendAllText(certFilePath, Environment.NewLine + keyPem);
 900            }
 1901        }
 0902        catch (Exception ex)
 903        {
 0904            Log.Debug(ex,
 0905                "Failed to append private key to certificate PEM file {CertFilePath}; continuing with separate key file 
 0906                certFilePath);
 0907        }
 1908    }
 909
 910
 911    /// <summary>
 912    /// Exports the specified X509 certificate to a file in the given format, using a SecureString password and optional
 913    /// </summary>
 914    /// <param name="cert">The X509Certificate2 to export.</param>
 915    /// <param name="filePath">The file path to export the certificate to.</param>
 916    /// <param name="fmt">The export format (Pfx or Pem).</param>
 917    /// <param name="password">The SecureString password to protect the exported certificate or private key, if applicab
 918    /// <param name="includePrivateKey">Whether to include the private key in the export.</param>
 919    public static void Export(
 920        X509Certificate2 cert,
 921        string filePath,
 922        ExportFormat fmt,
 923        SecureString password,
 924        bool includePrivateKey = false)
 925    {
 1926        if (password is null)
 927        {
 928            // Delegate to span-based overload with no password
 0929            Export(cert, filePath, fmt, [], includePrivateKey);
 930        }
 931        else
 932        {
 1933            password.ToSecureSpan(span =>
 1934                Export(cert, filePath, fmt, span, includePrivateKey)
 1935            // this will run your span‐based implementation,
 1936            // then immediately zero & free the unmanaged buffer
 1937            );
 938        }
 1939    }
 940
 941
 942    /// <summary>
 943    /// Creates a self-signed certificate from the given RSA JWK JSON and exports it
 944    /// as a PEM certificate (optionally including the private key) to the specified path.
 945    /// </summary>
 946    /// <param name="jwkJson">The RSA JWK JSON string.</param>
 947    /// <param name="filePath">
 948    /// Target file path. If no extension is provided, ".pem" will be added.
 949    /// </param>
 950    /// <param name="password">
 951    /// Optional password used to encrypt the private key when <paramref name="includePrivateKey"/> is true.
 952    /// Ignored when <paramref name="includePrivateKey"/> is false.
 953    /// </param>
 954    /// <param name="includePrivateKey">
 955    /// If true, the PEM export will include the private key (and create a .key file as per Export logic).
 956    /// </param>
 957    public static void ExportPemFromJwkJson(
 958        string jwkJson,
 959        string filePath,
 960        ReadOnlySpan<char> password = default,
 961        bool includePrivateKey = false)
 962    {
 0963        if (string.IsNullOrWhiteSpace(jwkJson))
 964        {
 0965            throw new ArgumentException("JWK JSON cannot be null or empty.", nameof(jwkJson));
 966        }
 967
 968        // 1) Create a self-signed certificate from the JWK
 0969        var cert = CreateSelfSignedCertificateFromJwk(jwkJson);
 970
 971        // 2) Reuse the existing Export pipeline to write PEM (cert + optional key)
 0972        Export(cert, filePath, ExportFormat.Pem, password, includePrivateKey);
 0973    }
 974
 975    /// <summary>
 976    /// Creates a self-signed certificate from the given RSA JWK JSON and exports it
 977    /// as a PEM certificate (optionally including the private key) to the specified path,
 978    /// using a <see cref="SecureString"/> password.
 979    /// </summary>
 980    /// <param name="jwkJson">The RSA JWK JSON string.</param>
 981    /// <param name="filePath">Target file path for the PEM output.</param>
 982    /// <param name="password">
 983    /// SecureString password used to encrypt the private key when
 984    /// <paramref name="includePrivateKey"/> is true.
 985    /// </param>
 986    /// <param name="includePrivateKey">
 987    /// If true, the PEM export will include the private key.
 988    /// </param>
 989    public static void ExportPemFromJwkJson(
 990        string jwkJson,
 991        string filePath,
 992        SecureString password,
 993        bool includePrivateKey = false)
 994    {
 0995        if (password is null)
 996        {
 997            // Delegate to span-based overload with no password
 0998            ExportPemFromJwkJson(jwkJson, filePath, [], includePrivateKey);
 0999            return;
 1000        }
 1001
 01002        password.ToSecureSpan(span =>
 01003        {
 01004            ExportPemFromJwkJson(jwkJson, filePath, span, includePrivateKey);
 01005        });
 01006    }
 1007
 1008
 1009    #endregion
 1010
 1011    #region JWK
 1012
 1013
 01014    private static readonly JsonSerializerOptions s_jwkJsonOptions = new()
 01015    {
 01016        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 01017        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 01018        WriteIndented = false
 01019    };
 1020
 1021    /// <summary>
 1022    /// Creates a self-signed X509 certificate from the provided RSA JWK JSON string.
 1023    /// </summary>
 1024    /// <param name="jwkJson">The JSON string representing the RSA JWK.</param>
 1025    /// <param name="subjectName">The subject name for the certificate.</param>
 1026    /// <returns>A self-signed X509Certificate2 instance.</returns>
 1027    /// <exception cref="ArgumentException">Thrown when the JWK JSON is invalid.</exception>
 1028    /// <exception cref="NotSupportedException"></exception>
 1029    public static X509Certificate2 CreateSelfSignedCertificateFromJwk(
 1030        string jwkJson,
 1031        string subjectName = "CN=client-jwt")
 1032    {
 01033        var jwk = JsonSerializer.Deserialize<RsaJwk>(jwkJson)
 01034                  ?? throw new ArgumentException("Invalid JWK JSON");
 1035
 01036        if (!string.Equals(jwk.Kty, "RSA", StringComparison.OrdinalIgnoreCase))
 1037        {
 01038            throw new NotSupportedException("Only RSA JWKs are supported.");
 1039        }
 1040
 01041        var rsaParams = new RSAParameters
 01042        {
 01043            Modulus = Base64UrlEncoder.DecodeBytes(jwk.N),
 01044            Exponent = Base64UrlEncoder.DecodeBytes(jwk.E),
 01045            D = Base64UrlEncoder.DecodeBytes(jwk.D),
 01046            P = Base64UrlEncoder.DecodeBytes(jwk.P),
 01047            Q = Base64UrlEncoder.DecodeBytes(jwk.Q),
 01048            DP = Base64UrlEncoder.DecodeBytes(jwk.DP),
 01049            DQ = Base64UrlEncoder.DecodeBytes(jwk.DQ),
 01050            InverseQ = Base64UrlEncoder.DecodeBytes(jwk.QI)
 01051        };
 1052
 01053        using var rsa = RSA.Create();
 01054        rsa.ImportParameters(rsaParams);
 1055
 01056        var req = new CertificateRequest(
 01057            subjectName,
 01058            rsa,
 01059            HashAlgorithmName.SHA256,
 01060            RSASignaturePadding.Pkcs1);
 1061
 1062        // Self-signed, 1 year validity (tune as you like)
 01063        var notBefore = DateTimeOffset.UtcNow.AddDays(-1);
 01064        var notAfter = notBefore.AddYears(1);
 1065
 01066        var cert = req.CreateSelfSigned(notBefore, notAfter);
 1067
 1068        // Export with private key, re-import as X509Certificate2
 01069        var pfxBytes = cert.Export(X509ContentType.Pfx);
 1070#if NET9_0_OR_GREATER
 1071        return X509CertificateLoader.LoadPkcs12(
 1072            pfxBytes,
 1073            password: default,
 1074            keyStorageFlags: X509KeyStorageFlags.Exportable,
 1075            loaderLimits: Pkcs12LoaderLimits.Defaults);
 1076#else
 01077        return new X509Certificate2(pfxBytes, (string?)null,
 01078            X509KeyStorageFlags.Exportable);
 1079#endif
 01080    }
 1081
 1082    /// <summary>
 1083    /// Builds a Private Key JWT for client authentication using the specified certificate.
 1084    /// </summary>
 1085    /// <param name="key">The security key (X509SecurityKey or JsonWebKey) to sign the JWT.</param>
 1086    /// <param name="clientId">The client ID (issuer and subject) for the JWT.</param>
 1087    /// <param name="tokenEndpoint">The token endpoint URL (audience) for the JWT.</param>
 1088    /// <returns>The generated Private Key JWT as a string.</returns>
 1089    public static string BuildPrivateKeyJwt(
 1090        SecurityKey key,
 1091        string clientId,
 1092        string tokenEndpoint)
 1093    {
 01094        var now = DateTimeOffset.UtcNow;
 1095
 01096        var creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
 01097        var handler = new JsonWebTokenHandler();
 1098
 01099        var descriptor = new SecurityTokenDescriptor
 01100        {
 01101            Issuer = clientId,
 01102            Audience = tokenEndpoint,
 01103            Subject = new ClaimsIdentity(
 01104            [
 01105                new Claim("sub", clientId),
 01106                new Claim("jti", Guid.NewGuid().ToString("N"))
 01107            ]),
 01108            NotBefore = now.UtcDateTime,
 01109            IssuedAt = now.UtcDateTime,
 01110            Expires = now.AddMinutes(2).UtcDateTime,
 01111            SigningCredentials = creds
 01112        };
 1113
 01114        return handler.CreateToken(descriptor);
 1115    }
 1116
 1117    /// <summary>
 1118    /// Builds a Private Key JWT for client authentication using the specified X509 certificate.
 1119    /// </summary>
 1120    /// <param name="certificate">The X509 certificate containing the private key.</param>
 1121    /// <param name="clientId">The client ID (issuer and subject) for the JWT.</param>
 1122    /// <param name="tokenEndpoint">The token endpoint URL (audience) for the JWT.</param>
 1123    /// <returns>The generated Private Key JWT as a string.</returns>
 1124    public static string BuildPrivateKeyJwt(
 1125        X509Certificate2 certificate,
 1126        string clientId,
 1127        string tokenEndpoint)
 1128    {
 01129        var key = new X509SecurityKey(certificate)
 01130        {
 01131            KeyId = certificate.Thumbprint
 01132        };
 1133
 01134        return BuildPrivateKeyJwt(key, clientId, tokenEndpoint);
 1135    }
 1136
 1137    /// <summary>
 1138    /// Builds a Private Key JWT for client authentication using the specified JWK JSON string.
 1139    /// </summary>
 1140    /// <param name="jwkJson">The JWK JSON string representing the key.</param>
 1141    /// <param name="clientId">The client ID (issuer and subject) for the JWT.</param>
 1142    /// <param name="tokenEndpoint">The token endpoint URL (audience) for the JWT.</param>
 1143    /// <returns>The generated Private Key JWT as a string.</returns>
 1144    public static string BuildPrivateKeyJwtFromJwkJson(
 1145        string jwkJson,
 1146        string clientId,
 1147        string tokenEndpoint)
 1148    {
 01149        var jwk = new JsonWebKey(jwkJson);
 1150        // You can set KeyId here if you want to use kid from the JSON:
 1151        // jwk.KeyId is automatically populated from "kid" if present.
 1152
 01153        return BuildPrivateKeyJwt(jwk, clientId, tokenEndpoint);
 1154    }
 1155
 1156
 1157    /// <summary>
 1158    /// Builds a JWK JSON (RSA) representation of the given certificate.
 1159    /// By default only public parameters are included (safe for publishing as JWKS).
 1160    /// Set <paramref name="includePrivateParameters"/> to true if you want a full private JWK
 1161    /// (for local storage only – never publish it).
 1162    /// </summary>
 1163    /// <param name="certificate">The X509 certificate to convert.</param>
 1164    /// <param name="includePrivateParameters">Whether to include private key parameters in the JWK.</param>
 1165    /// <returns>The JWK JSON string.</returns>
 1166    public static string CreateJwkJsonFromCertificate(
 1167       X509Certificate2 certificate,
 1168       bool includePrivateParameters = false)
 1169    {
 01170        var x509Key = new X509SecurityKey(certificate)
 01171        {
 01172            KeyId = certificate.Thumbprint?.ToLowerInvariant()
 01173        };
 1174
 1175        // Convert to a JsonWebKey (n, e, kid, x5c, etc.)
 01176        var jwk = JsonWebKeyConverter.ConvertFromX509SecurityKey(
 01177            x509Key,
 01178            representAsRsaKey: true);
 1179
 01180        if (!includePrivateParameters)
 1181        {
 1182            // Clean public JWK
 01183            jwk.D = null;
 01184            jwk.P = null;
 01185            jwk.Q = null;
 01186            jwk.DP = null;
 01187            jwk.DQ = null;
 01188            jwk.QI = null;
 1189        }
 1190        else
 1191        {
 01192            if (!certificate.HasPrivateKey)
 1193            {
 01194                throw new InvalidOperationException("Certificate has no private key.");
 1195            }
 1196
 01197            using var rsa = certificate.GetRSAPrivateKey()
 01198                ?? throw new NotSupportedException("Certificate does not contain an RSA private key.");
 1199
 01200            var p = rsa.ExportParameters(true);
 1201
 01202            jwk.N = Base64UrlEncoder.Encode(p.Modulus);
 01203            jwk.E = Base64UrlEncoder.Encode(p.Exponent);
 01204            jwk.D = Base64UrlEncoder.Encode(p.D);
 01205            jwk.P = Base64UrlEncoder.Encode(p.P);
 01206            jwk.Q = Base64UrlEncoder.Encode(p.Q);
 01207            jwk.DP = Base64UrlEncoder.Encode(p.DP);
 01208            jwk.DQ = Base64UrlEncoder.Encode(p.DQ);
 01209            jwk.QI = Base64UrlEncoder.Encode(p.InverseQ);
 1210        }
 1211
 01212        return JsonSerializer.Serialize(jwk, s_jwkJsonOptions);
 1213    }
 1214
 1215    /// <summary>
 1216    /// Creates an RSA JWK JSON from a given RSA instance (must contain private key).
 1217    /// </summary>
 1218    /// <param name="rsa">The RSA instance with a private key.</param>
 1219    /// <param name="keyId">Optional key identifier (kid) to set on the JWK.</param>
 1220    /// <returns>JWK JSON string containing public and private parameters.</returns>
 1221    public static string CreateJwkJsonFromRsa(RSA rsa, string? keyId = null)
 1222    {
 01223        ArgumentNullException.ThrowIfNull(rsa);
 1224
 1225        // true => includes private key params (d, p, q, dp, dq, qi)
 01226        var p = rsa.ExportParameters(includePrivateParameters: true);
 1227
 01228        if (p.D is null || p.P is null || p.Q is null ||
 01229            p.DP is null || p.DQ is null || p.InverseQ is null)
 1230        {
 01231            throw new InvalidOperationException("RSA key does not contain private parameters.");
 1232        }
 1233
 01234        var jwk = new RsaJwk
 01235        {
 01236            Kty = "RSA",
 01237            N = Base64UrlEncoder.Encode(p.Modulus),
 01238            E = Base64UrlEncoder.Encode(p.Exponent),
 01239            D = Base64UrlEncoder.Encode(p.D),
 01240            P = Base64UrlEncoder.Encode(p.P),
 01241            Q = Base64UrlEncoder.Encode(p.Q),
 01242            DP = Base64UrlEncoder.Encode(p.DP),
 01243            DQ = Base64UrlEncoder.Encode(p.DQ),
 01244            QI = Base64UrlEncoder.Encode(p.InverseQ),
 01245            Kid = keyId
 01246        };
 1247
 01248        return JsonSerializer.Serialize(jwk, s_jwkJsonOptions);
 1249    }
 1250
 1251    /// <summary>
 1252    /// Creates an RSA JWK JSON from a PKCS#1 or PKCS#8 RSA private key in PEM format.
 1253    /// </summary>
 1254    /// <param name="rsaPrivateKeyPem">
 1255    /// PEM containing an RSA private key (e.g. "-----BEGIN RSA PRIVATE KEY----- ...").
 1256    /// </param>
 1257    /// <param name="keyId">Optional key identifier (kid) to set on the JWK.</param>
 1258    /// <returns>JWK JSON string containing public and private parameters.</returns>
 1259    public static string CreateJwkJsonFromRsaPrivateKeyPem(
 1260        string rsaPrivateKeyPem,
 1261        string? keyId = null)
 1262    {
 01263        if (string.IsNullOrWhiteSpace(rsaPrivateKeyPem))
 1264        {
 01265            throw new ArgumentException("RSA private key PEM cannot be null or empty.", nameof(rsaPrivateKeyPem));
 1266        }
 1267
 01268        using var rsa = RSA.Create();
 01269        rsa.ImportFromPem(rsaPrivateKeyPem.AsSpan());
 1270
 01271        return CreateJwkJsonFromRsa(rsa, keyId);
 01272    }
 1273
 1274
 1275
 1276    #endregion
 1277
 1278    #region  Validation helpers (Test-PodeCertificate equivalent)
 1279    /// <summary>
 1280    /// Validates the specified X509 certificate according to the provided options.
 1281    /// </summary>
 1282    /// <param name="cert">The X509Certificate2 to validate.</param>
 1283    /// <param name="checkRevocation">Whether to check certificate revocation status.</param>
 1284    /// <param name="allowWeakAlgorithms">Whether to allow weak algorithms such as SHA-1 or small key sizes.</param>
 1285    /// <param name="denySelfSigned">Whether to deny self-signed certificates.</param>
 1286    /// <param name="expectedPurpose">A collection of expected key purposes (EKU) for the certificate.</param>
 1287    /// <param name="strictPurpose">If true, the certificate must match the expected purposes exactly.</param>
 1288    /// <returns>True if the certificate is valid according to the specified options; otherwise, false.</returns>
 1289    public static bool Validate(
 1290     X509Certificate2 cert,
 1291     bool checkRevocation = false,
 1292     bool allowWeakAlgorithms = false,
 1293     bool denySelfSigned = false,
 1294     OidCollection? expectedPurpose = null,
 1295     bool strictPurpose = false)
 1296    {
 1297        // 1) Validity period
 71298        if (!IsWithinValidityPeriod(cert))
 1299        {
 01300            return false;
 1301        }
 1302
 1303        // 2) Self-signed policy
 71304        var isSelfSigned = cert.Subject == cert.Issuer;
 71305        if (denySelfSigned && isSelfSigned)
 1306        {
 11307            return false;
 1308        }
 1309
 1310        // 3) Chain build (with optional revocation)
 61311        if (!BuildChainOk(cert, checkRevocation, isSelfSigned))
 1312        {
 01313            return false;
 1314        }
 1315
 1316        // 4) EKU / purposes
 61317        if (!PurposesOk(cert, expectedPurpose, strictPurpose))
 1318        {
 11319            return false;
 1320        }
 1321
 1322        // 5) Weak algorithms
 51323        if (!allowWeakAlgorithms && UsesWeakAlgorithms(cert))
 1324        {
 11325            return false;
 1326        }
 1327
 41328        return true;   // ✅ everything passed
 1329    }
 1330
 1331    /// <summary>
 1332    /// Checks if the certificate is within its validity period.
 1333    /// </summary>
 1334    /// <param name="cert">The X509Certificate2 to check.</param>
 1335    /// <returns>True if the certificate is within its validity period; otherwise, false.</returns>
 1336    private static bool IsWithinValidityPeriod(X509Certificate2 cert)
 71337        => DateTime.UtcNow >= cert.NotBefore && DateTime.UtcNow <= cert.NotAfter;
 1338
 1339    /// <summary>
 1340    /// Checks if the certificate chain is valid.
 1341    /// </summary>
 1342    /// <param name="cert">The X509Certificate2 to check.</param>
 1343    /// <param name="checkRevocation">Whether to check certificate revocation status.</param>
 1344    /// <param name="isSelfSigned">Whether the certificate is self-signed.</param>
 1345    /// <returns>True if the certificate chain is valid; otherwise, false.</returns>
 1346    private static bool BuildChainOk(X509Certificate2 cert, bool checkRevocation, bool isSelfSigned)
 1347    {
 61348        using var chain = new X509Chain();
 61349        chain.ChainPolicy.RevocationMode = checkRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck;
 61350        chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EndCertificateOnly;
 61351        chain.ChainPolicy.DisableCertificateDownloads = !checkRevocation;
 1352
 61353        if (isSelfSigned)
 1354        {
 61355            chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
 1356        }
 1357
 61358        return chain.Build(cert);
 61359    }
 1360
 1361    /// <summary>
 1362    /// Checks if the certificate has the expected key purposes (EKU).
 1363    /// </summary>
 1364    /// <param name="cert">The X509Certificate2 to check.</param>
 1365    /// <param name="expectedPurpose">A collection of expected key purposes (EKU) for the certificate.</param>
 1366    /// <param name="strictPurpose">If true, the certificate must match the expected purposes exactly.</param>
 1367    /// <returns>True if the certificate has the expected purposes; otherwise, false.</returns>
 1368    private static bool PurposesOk(X509Certificate2 cert, OidCollection? expectedPurpose, bool strictPurpose)
 1369    {
 61370        if (expectedPurpose is not { Count: > 0 })
 1371        {
 31372            return true; // nothing to check
 1373        }
 1374
 31375        var eku = cert.Extensions
 31376                       .OfType<X509EnhancedKeyUsageExtension>()
 31377                       .SelectMany(e => e.EnhancedKeyUsages.Cast<Oid>())
 61378                       .Select(o => o.Value)
 31379                       .ToHashSet();
 1380
 31381        var wanted = expectedPurpose.Cast<Oid>()
 41382                                    .Select(o => o.Value)
 31383                                    .ToHashSet();
 1384
 31385        return strictPurpose ? eku.SetEquals(wanted) : wanted.All(eku.Contains);
 1386    }
 1387
 1388    /// <summary>
 1389    /// Checks if the certificate uses weak algorithms.
 1390    /// </summary>
 1391    /// <param name="cert">The X509Certificate2 to check.</param>
 1392    /// <returns>True if the certificate uses weak algorithms; otherwise, false.</returns>
 1393    private static bool UsesWeakAlgorithms(X509Certificate2 cert)
 1394    {
 41395        var isSha1 = cert.SignatureAlgorithm?.FriendlyName?
 41396                           .Contains("sha1", StringComparison.OrdinalIgnoreCase) == true;
 1397
 41398        var weakRsa = cert.GetRSAPublicKey() is { KeySize: < 2048 };
 41399        var weakDsa = cert.GetDSAPublicKey() is { KeySize: < 2048 };
 41400        var weakEcdsa = cert.GetECDsaPublicKey() is { KeySize: < 256 };  // P-256 minimum
 1401
 41402        return isSha1 || weakRsa || weakDsa || weakEcdsa;
 1403    }
 1404
 1405
 1406    /// <summary>
 1407    /// Gets the enhanced key usage purposes (EKU) from the specified X509 certificate.
 1408    /// </summary>
 1409    /// <param name="cert">The X509Certificate2 to extract purposes from.</param>
 1410    /// <returns>An enumerable of purpose names or OID values.</returns>
 1411    public static IEnumerable<string> GetPurposes(X509Certificate2 cert) =>
 11412        cert.Extensions
 11413            .OfType<X509EnhancedKeyUsageExtension>()
 11414            .SelectMany(x => x.EnhancedKeyUsages.Cast<Oid>())
 21415            .Select(o => (o.FriendlyName ?? o.Value)!)   // ← null-forgiving
 31416            .Where(s => s.Length > 0);                   // optional: drop empties
 1417    #endregion
 1418
 1419    #region  private helpers
 1420    private static AsymmetricCipherKeyPair GenRsaKeyPair(int bits, SecureRandom rng)
 1421    {
 131422        var gen = new RsaKeyPairGenerator();
 131423        gen.Init(new KeyGenerationParameters(rng, bits));
 131424        return gen.GenerateKeyPair();
 1425    }
 1426
 1427    /// <summary>
 1428    /// Generates an EC key pair.
 1429    /// </summary>
 1430    /// <param name="bits">The key size in bits.</param>
 1431    /// <param name="rng">The secure random number generator.</param>
 1432    /// <returns>The generated EC key pair.</returns>
 1433    private static AsymmetricCipherKeyPair GenEcKeyPair(int bits, SecureRandom rng)
 1434    {
 1435        // NIST-style names are fine here
 11436        var name = bits switch
 11437        {
 11438            <= 256 => "P-256",
 01439            <= 384 => "P-384",
 01440            _ => "P-521"
 11441        };
 1442
 1443        // ECNamedCurveTable knows about SEC *and* NIST names
 11444        var ecParams = ECNamedCurveTable.GetByName(name)
 11445                       ?? throw new InvalidOperationException($"Curve not found: {name}");
 1446
 11447        var domain = new ECDomainParameters(
 11448            ecParams.Curve, ecParams.G, ecParams.N, ecParams.H, ecParams.GetSeed());
 1449
 11450        var gen = new ECKeyPairGenerator();
 11451        gen.Init(new ECKeyGenerationParameters(domain, rng));
 11452        return gen.GenerateKeyPair();
 1453    }
 1454
 1455    /// <summary>
 1456    /// Converts a BouncyCastle X509Certificate to a .NET X509Certificate2.
 1457    /// </summary>
 1458    /// <param name="cert">The BouncyCastle X509Certificate to convert.</param>
 1459    /// <param name="privKey">The private key associated with the certificate.</param>
 1460    /// <param name="flags">The key storage flags to use.</param>
 1461    /// <param name="ephemeral">Whether the key is ephemeral.</param>
 1462    /// <returns></returns>
 1463    private static X509Certificate2 ToX509Cert2(
 1464        Org.BouncyCastle.X509.X509Certificate cert,
 1465        AsymmetricKeyParameter privKey,
 1466        X509KeyStorageFlags flags,
 1467        bool ephemeral)
 1468    {
 121469        var store = new Pkcs12StoreBuilder().Build();
 121470        var entry = new X509CertificateEntry(cert);
 1471        const string alias = "cert";
 121472        store.SetCertificateEntry(alias, entry);
 121473        store.SetKeyEntry(alias, new AsymmetricKeyEntry(privKey),
 121474                          [entry]);
 1475
 121476        using var ms = new MemoryStream();
 121477        store.Save(ms, [], new SecureRandom());
 121478        var raw = ms.ToArray();
 1479
 1480#if NET9_0_OR_GREATER
 1481        try
 1482        {
 1483            return X509CertificateLoader.LoadPkcs12(
 1484                raw,
 1485                password: default,
 1486                keyStorageFlags: flags | (ephemeral ? X509KeyStorageFlags.EphemeralKeySet : 0),
 1487                loaderLimits: Pkcs12LoaderLimits.Defaults
 1488            );
 1489        }
 1490        catch (PlatformNotSupportedException) when (ephemeral)
 1491        {
 1492            // Some platforms (e.g. certain Linux/macOS runners) don't yet support
 1493            // EphemeralKeySet with the new X509CertificateLoader API. In that case
 1494            // we fall back to re-loading without the EphemeralKeySet flag. The
 1495            // intent of Ephemeral in our API is simply "do not persist in a store" –
 1496            // loading without the flag here still keeps the cert in-memory only.
 1497            Log.Debug("EphemeralKeySet not supported on this platform for X509CertificateLoader; falling back without th
 1498            return X509CertificateLoader.LoadPkcs12(
 1499                raw,
 1500                password: default,
 1501                keyStorageFlags: flags, // omit EphemeralKeySet
 1502                loaderLimits: Pkcs12LoaderLimits.Defaults
 1503            );
 1504        }
 1505#else
 1506        try
 1507        {
 121508            return new X509Certificate2(
 121509                raw,
 121510                (string?)null,
 121511                flags | (ephemeral ? X509KeyStorageFlags.EphemeralKeySet : 0)
 121512            );
 1513        }
 01514        catch (PlatformNotSupportedException) when (ephemeral)
 1515        {
 1516            // macOS (and some Linux distros) under net8 may not support EphemeralKeySet here.
 01517            Log.Debug("EphemeralKeySet not supported on this platform (net8); falling back without the flag.");
 01518            return new X509Certificate2(
 01519                raw,
 01520                (string?)null,
 01521                flags // omit EphemeralKeySet
 01522            );
 1523        }
 1524
 1525#endif
 121526    }
 1527
 1528    #endregion
 1529}

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)
PurposesOk(System.Security.Cryptography.X509Certificates.X509Certificate2,System.Security.Cryptography.OidCollection,System.Boolean)
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)