feat: enhance certificate handling with BouncyCastle and remove firmware version checks

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Neil Dorin 2026-05-07 09:01:27 -06:00
parent 1b32761e8e
commit 89c23f5432
2 changed files with 77 additions and 21 deletions

View file

@ -15,10 +15,12 @@ using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Operators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;
using System.Security.Cryptography;
using Serilog.Formatting;
using Serilog.Formatting.Json;
@ -172,7 +174,15 @@ namespace PepperDash.Core
using (var ms = new MemoryStream())
{
pkcs12Store.Save(ms, _certificatePassword.ToCharArray(), random);
var passwordChars = _certificatePassword.ToCharArray();
try
{
pkcs12Store.Save(ms, passwordChars, random);
}
finally
{
Array.Clear(passwordChars, 0, passwordChars.Length);
}
File.WriteAllBytes(outputPath, ms.ToArray());
}
@ -215,23 +225,69 @@ namespace PepperDash.Core
private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword)
{
if (!File.Exists(certPath))
CreateCert();
try
{
// EphemeralKeySet is required on Linux/OpenSSL (Crestron 4-series) to avoid
// key-container persistence failures, and avoids the private key export restriction.
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
return LoadCertFromBouncyCastle(certPath, certPassword);
}
catch (Exception ex)
{
// Cert is stale or was generated by an incompatible library (e.g. old BouncyCastle output).
// Delete it, regenerate with the BCL path, and retry once.
// Cert is corrupt or was written by an incompatible tool — delete and regenerate once.
CrestronConsole.PrintLine(string.Format("SSL cert load failed ({0}); regenerating...", ex.Message));
try { File.Delete(certPath); } catch { }
CreateCert();
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
return LoadCertFromBouncyCastle(certPath, certPassword);
}
}
/// <summary>
/// Loads a PKCS#12 file written by BouncyCastle and returns an <see cref="X509Certificate2"/> with
/// private key attached via <see cref="RSACryptoServiceProvider"/>.
/// Using BouncyCastle's own reader avoids the .NET/Mono PFX parser, which can reject
/// BouncyCastle-generated archives on the Crestron runtime.
/// </summary>
private static X509Certificate2 LoadCertFromBouncyCastle(string certPath, string certPassword)
{
var passwordChars = certPassword.ToCharArray();
try
{
using (var stream = File.OpenRead(certPath))
{
var store = new Pkcs12StoreBuilder().Build();
store.Load(stream, passwordChars);
foreach (string alias in store.Aliases)
{
if (!store.IsKeyEntry(alias)) continue;
var keyEntry = store.GetKey(alias);
var certChain = store.GetCertificateChain(alias);
if (certChain == null || certChain.Length == 0) continue;
// Build X509Certificate2 from raw DER — no PFX parsing by .NET needed.
var cert = new X509Certificate2(certChain[0].Certificate.GetEncoded());
// Attach the private key via RSACryptoServiceProvider (available on all target runtimes).
var rsaParams = DotNetUtilities.ToRSAParameters(
(RsaPrivateCrtKeyParameters)keyEntry.Key);
var rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(rsaParams);
cert.PrivateKey = rsa;
return cert;
}
}
}
finally
{
Array.Clear(passwordChars, 0, passwordChars.Length);
}
throw new InvalidOperationException("No key entry found in PKCS#12 store: " + certPath);
}
private void Start(int port, string certPath = "", string certPassword = "")
{
try