Merge pull request #1418 from PepperDash/fix/devtools-fixex

Fix/devtools fixex
This commit is contained in:
Neil Dorin 2026-05-11 10:59:58 -06:00 committed by GitHub
commit beb77ec468
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 62 additions and 24 deletions

View file

@ -15,12 +15,10 @@ using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Operators; using Org.BouncyCastle.Crypto.Operators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math; using Org.BouncyCastle.Math;
using Org.BouncyCastle.Pkcs; using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security; using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509; using Org.BouncyCastle.X509;
using System.Security.Cryptography;
using Serilog.Formatting; using Serilog.Formatting;
using Serilog.Formatting.Json; using Serilog.Formatting.Json;
@ -244,9 +242,10 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Loads a PKCS#12 file written by BouncyCastle and returns an <see cref="X509Certificate2"/> with /// Loads a PKCS#12 file written by BouncyCastle and returns an <see cref="X509Certificate2"/> with
/// private key attached via <see cref="RSACryptoServiceProvider"/>. /// private key attached.
/// Using BouncyCastle's own reader avoids the .NET/Mono PFX parser, which can reject /// The PFX is parsed and re-encoded by BouncyCastle (ensuring format compatibility), then passed as
/// BouncyCastle-generated archives on the Crestron runtime. /// raw bytes to <see cref="X509Certificate2"/> so neither <c>RSACryptoServiceProvider</c> nor the
/// <c>EphemeralKeySet</c> flag (unsupported on the Crestron/Mono runtime) is needed.
/// </summary> /// </summary>
private static X509Certificate2 LoadCertFromBouncyCastle(string certPath, string certPassword) private static X509Certificate2 LoadCertFromBouncyCastle(string certPath, string certPassword)
{ {
@ -258,24 +257,16 @@ namespace PepperDash.Core
var store = new Pkcs12StoreBuilder().Build(); var store = new Pkcs12StoreBuilder().Build();
store.Load(stream, passwordChars); 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; store.Save(ms, passwordChars, new SecureRandom());
var cert = new X509Certificate2(ms.ToArray(), certPassword);
var keyEntry = store.GetKey(alias); if (!cert.HasPrivateKey)
var certChain = store.GetCertificateChain(alias); throw new InvalidOperationException(
if (certChain == null || certChain.Length == 0) continue; string.Format("Certificate loaded from '{0}' does not contain a private key and cannot be used as a server certificate.", certPath));
// 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; return cert;
} }
@ -285,8 +276,6 @@ namespace PepperDash.Core
{ {
Array.Clear(passwordChars, 0, passwordChars.Length); 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 = "") private void Start(int port, string certPath = "", string certPassword = "")

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic;
using Crestron.SimplSharp.CrestronAuthentication; using Crestron.SimplSharp.CrestronAuthentication;
using Crestron.SimplSharp.WebScripting; using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -91,7 +92,19 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
context.Response.StatusDescription = "OK"; context.Response.StatusDescription = "OK";
context.Response.ContentType = "application/json"; context.Response.ContentType = "application/json";
context.Response.ContentEncoding = System.Text.Encoding.UTF8; context.Response.ContentEncoding = System.Text.Encoding.UTF8;
context.Response.Write(JsonConvert.SerializeObject(new { Token = token }, Formatting.Indented), false); context.Response.Write(JsonConvert.SerializeObject(
new
{
Token = new LoginResponse
{
UserName = token.UserName,
Access = token.Access,
State = token.State,
Groups = token.Groups,
ADConnect = token.ADConnect,
Valid = token.Valid
}
}, Formatting.Indented), false);
context.Response.End(); context.Response.End();
} }
catch (System.Exception ex) catch (System.Exception ex)
@ -121,4 +134,40 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
/// </summary> /// </summary>
public string Password { get; set; } public string Password { get; set; }
} }
/// <summary>
/// Represents a LoginResponse
/// </summary>
internal class LoginResponse
{
/// <summary>
/// Gets or sets the username.
/// </summary>
public string UserName { get; set; }
/// <summary>
/// Gets or sets the access level.
/// </summary>
public Authentication.UserAuthenticationLevelEnum Access { get; set; }
/// <summary>
/// Gets or sets the token authenticated state.
/// </summary>
public Authentication.eTokenAuthenticatedState State { get; set; }
/// <summary>
/// Gets or sets the list of groups.
/// </summary>
public List<string> Groups { get; set; }
/// <summary>
/// Gets or sets the active directory connection flag.
/// </summary>
public int ADConnect { get; set; }
/// <summary>
/// Gets or sets the valid flag indicating whether the token is valid.
/// </summary>
public bool Valid { get; set; }
}
} }