From e5e1802da996433b4761fdc58c623cf13d787f93 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Sun, 10 May 2026 20:48:05 -0600 Subject: [PATCH 1/5] feat: enhance LoginRequestHandler to include detailed LoginResponse structure Co-authored-by: Copilot --- .../Logging/DebugWebsocketSink.cs | 35 ++++--------- .../RequestHandlers/LoginRequestHandler.cs | 52 ++++++++++++++++++- 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index 8986fcb4..99910ec9 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -15,12 +15,10 @@ 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; @@ -244,9 +242,10 @@ namespace PepperDash.Core /// /// Loads a PKCS#12 file written by BouncyCastle and returns an with - /// private key attached via . - /// Using BouncyCastle's own reader avoids the .NET/Mono PFX parser, which can reject - /// BouncyCastle-generated archives on the Crestron runtime. + /// private key attached. + /// The PFX is parsed and re-encoded by BouncyCastle (ensuring format compatibility), then passed as + /// raw bytes to so neither RSACryptoServiceProvider nor the + /// EphemeralKeySet flag (unsupported on the Crestron/Mono runtime) is needed. /// private static X509Certificate2 LoadCertFromBouncyCastle(string certPath, string certPassword) { @@ -258,26 +257,12 @@ namespace PepperDash.Core var store = new Pkcs12StoreBuilder().Build(); store.Load(stream, passwordChars); - foreach (string alias in store.Aliases) + // Re-encode through BouncyCastle to guarantee PKCS#12 format compatibility, + // then hand raw bytes to X509Certificate2 — no RSACryptoServiceProvider needed. + using (var ms = new MemoryStream()) { - 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.PersistKeyInCsp = false; - rsa.ImportParameters(rsaParams); - cert.PrivateKey = rsa; - - return cert; + store.Save(ms, passwordChars, new SecureRandom()); + return new X509Certificate2(ms.ToArray(), certPassword); } } } @@ -285,8 +270,6 @@ namespace PepperDash.Core { 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 = "") diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs index 5a4b7df7..c05e0cb6 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs @@ -1,5 +1,6 @@ using System; +using System.Collections.Generic; using Crestron.SimplSharp.CrestronAuthentication; using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; @@ -91,7 +92,15 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers context.Response.StatusDescription = "OK"; context.Response.ContentType = "application/json"; context.Response.ContentEncoding = System.Text.Encoding.UTF8; - context.Response.Write(JsonConvert.SerializeObject(new { Token = token }, Formatting.Indented), false); + context.Response.Write(JsonConvert.SerializeObject( + new LoginResponse + { UserName = token.UserName, + Password = token.Password, + Access = token.Access, + State = token.State, + Groups = token.Groups, + ADConnect = token.ADConnect, + Valid = token.Valid }, Formatting.Indented), false); context.Response.End(); } catch (System.Exception ex) @@ -121,4 +130,45 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers /// public string Password { get; set; } } + + /// + /// Represents a LoginResponse + /// + internal class LoginResponse + { + /// + /// Gets or sets the username. + /// + public string UserName { get; set; } + + /// + /// Gets or sets the password. + /// + public string Password { get; set; } + + /// + /// Gets or sets the access level. + /// + public Authentication.UserAuthenticationLevelEnum Access { get; set; } + + /// + /// Gets or sets the token authenticated state. + /// + public Authentication.eTokenAuthenticatedState State { get; set; } + + /// + /// Gets or sets the list of groups. + /// + public List Groups { get; set; } + + /// + /// Gets or sets the active directory connection flag. + /// + public int ADConnect { get; set; } + + /// + /// Gets or sets the valid flag indicating whether the token is valid. + /// + public bool Valid { get; set; } + } } \ No newline at end of file From 1a5840c29a166425baab9f95fbc94ddfa130c75c Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 11 May 2026 09:48:12 -0600 Subject: [PATCH 2/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Web/RequestHandlers/LoginRequestHandler.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs index c05e0cb6..f70d1ddb 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs @@ -93,14 +93,15 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers context.Response.ContentType = "application/json"; context.Response.ContentEncoding = System.Text.Encoding.UTF8; context.Response.Write(JsonConvert.SerializeObject( - new LoginResponse - { UserName = token.UserName, - Password = token.Password, - Access = token.Access, - State = token.State, - Groups = token.Groups, - ADConnect = token.ADConnect, - Valid = token.Valid }, Formatting.Indented), false); + new + { + UserName = token.UserName, + Access = token.Access, + State = token.State, + Groups = token.Groups, + ADConnect = token.ADConnect, + Valid = token.Valid + }, Formatting.Indented), false); context.Response.End(); } catch (System.Exception ex) From d879430616fad5255941d50fca218e9923f56020 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 11 May 2026 09:50:39 -0600 Subject: [PATCH 3/5] feat: update LoginResponse structure to include Password in the token response --- .../RequestHandlers/LoginRequestHandler.cs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs index f70d1ddb..3d9fdac3 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs @@ -93,15 +93,19 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers context.Response.ContentType = "application/json"; context.Response.ContentEncoding = System.Text.Encoding.UTF8; context.Response.Write(JsonConvert.SerializeObject( - new - { - UserName = token.UserName, - Access = token.Access, - State = token.State, - Groups = token.Groups, - ADConnect = token.ADConnect, - Valid = token.Valid - }, Formatting.Indented), false); + new + { + Token = new LoginResponse + { + UserName = token.UserName, + Password = token.Password, + Access = token.Access, + State = token.State, + Groups = token.Groups, + ADConnect = token.ADConnect, + Valid = token.Valid + } + }, Formatting.Indented), false); context.Response.End(); } catch (System.Exception ex) From a9dd57fdaf70b55f63bf7b4a3c2e2088aee244e7 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 11 May 2026 09:51:48 -0600 Subject: [PATCH 4/5] refactor: remove Password from LoginResponse and related token assignment --- .../Web/RequestHandlers/LoginRequestHandler.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs index 3d9fdac3..949f8bd5 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs @@ -98,7 +98,6 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers Token = new LoginResponse { UserName = token.UserName, - Password = token.Password, Access = token.Access, State = token.State, Groups = token.Groups, @@ -146,11 +145,6 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers /// public string UserName { get; set; } - /// - /// Gets or sets the password. - /// - public string Password { get; set; } - /// /// Gets or sets the access level. /// From b318e7f365d5e405d84a62ab5164add4fde4ee64 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 11 May 2026 09:55:27 -0600 Subject: [PATCH 5/5] fix: validate certificate for private key presence in DebugWebsocketSink Co-authored-by: Copilot --- src/PepperDash.Core/Logging/DebugWebsocketSink.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index 99910ec9..eeba5772 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -262,7 +262,13 @@ namespace PepperDash.Core using (var ms = new MemoryStream()) { store.Save(ms, passwordChars, new SecureRandom()); - return new X509Certificate2(ms.ToArray(), certPassword); + var cert = new X509Certificate2(ms.ToArray(), certPassword); + + if (!cert.HasPrivateKey) + throw new InvalidOperationException( + string.Format("Certificate loaded from '{0}' does not contain a private key and cannot be used as a server certificate.", certPath)); + + return cert; } } }