mirror of
https://github.com/PepperDash/PepperDashCore.git
synced 2026-02-16 13:14:49 +00:00
feat: adds BouncyCertificate
This commit is contained in:
@@ -9,39 +9,43 @@ using Serilog.Events;
|
|||||||
using Serilog.Configuration;
|
using Serilog.Configuration;
|
||||||
using WebSocketSharp.Server;
|
using WebSocketSharp.Server;
|
||||||
using Crestron.SimplSharp;
|
using Crestron.SimplSharp;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using WebSocketSharp;
|
using WebSocketSharp;
|
||||||
|
using System.Security.Authentication;
|
||||||
|
using WebSocketSharp.Net;
|
||||||
|
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
|
||||||
|
using System.IO;
|
||||||
|
using Org.BouncyCastle.Asn1.X509;
|
||||||
|
|
||||||
namespace PepperDash.Core
|
namespace PepperDash.Core
|
||||||
{
|
{
|
||||||
public class DebugWebsocketSink : ILogEventSink
|
public class DebugWebsocketSink : ILogEventSink
|
||||||
{
|
{
|
||||||
private HttpServer _server;
|
private HttpServer _httpsServer;
|
||||||
|
|
||||||
private string _path = "/join";
|
private string _path = "/join";
|
||||||
|
private const string _certificateName = "selfCres";
|
||||||
|
private const string _certificatePassword = "cres12345";
|
||||||
|
|
||||||
public int Port
|
public int Port
|
||||||
{ get
|
{ get
|
||||||
{
|
{
|
||||||
|
|
||||||
if(_server == null) return 0;
|
if(_httpsServer == null) return 0;
|
||||||
return _server.Port;
|
return _httpsServer.Port;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsListening
|
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
|
||||||
{ get
|
|
||||||
{
|
|
||||||
if (_server == null) return false;
|
|
||||||
return _server.IsListening;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly IFormatProvider _formatProvider;
|
private readonly IFormatProvider _formatProvider;
|
||||||
|
|
||||||
public DebugWebsocketSink()
|
public DebugWebsocketSink()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
if (!File.Exists($"\\user\\{_certificateName}.pfx"))
|
||||||
|
CreateCert(null);
|
||||||
|
|
||||||
CrestronEnvironment.ProgramStatusEventHandler += type =>
|
CrestronEnvironment.ProgramStatusEventHandler += type =>
|
||||||
{
|
{
|
||||||
if (type == eProgramStatusEventType.Stopping)
|
if (type == eProgramStatusEventType.Stopping)
|
||||||
@@ -51,26 +55,133 @@ namespace PepperDash.Core
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CreateCert(string[] args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Debug.Console(0,$"CreateCert Creating Utility");
|
||||||
|
//var utility = new CertificateUtility();
|
||||||
|
var utility = new BouncyCertificate();
|
||||||
|
Debug.Console(0, $"CreateCert Calling CreateCert");
|
||||||
|
//utility.CreateCert();
|
||||||
|
var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
|
||||||
|
var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
|
||||||
|
var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0);
|
||||||
|
|
||||||
|
Debug.Console(0, $"DomainName: {domainName} | HostName: {hostName} | {hostName}.{domainName}@{ipAddress}");
|
||||||
|
|
||||||
|
var certificate = utility.CreateSelfSignedCertificate($"CN={hostName}.{domainName}", new[] { $"{hostName}.{domainName}", ipAddress }, new[] { KeyPurposeID.IdKPServerAuth, KeyPurposeID.IdKPClientAuth });
|
||||||
|
//Crestron fails to let us do this...perhaps it should be done through their Dll's but haven't tested
|
||||||
|
//Debug.Print($"CreateCert Storing Certificate To My.LocalMachine");
|
||||||
|
//utility.AddCertToStore(certificate, StoreName.My, StoreLocation.LocalMachine);
|
||||||
|
Debug.Console(0, $"CreateCert Saving Cert to \\user\\");
|
||||||
|
utility.CertificatePassword = _certificatePassword;
|
||||||
|
utility.WriteCertificate(certificate, @"\user\", _certificateName);
|
||||||
|
Debug.Console(0, $"CreateCert Ending CreateCert");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.Console(0, $"WSS CreateCert Failed\r\n{ex.Message}\r\n{ex.StackTrace}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Emit(LogEvent logEvent)
|
public void Emit(LogEvent logEvent)
|
||||||
{
|
{
|
||||||
if (_server == null || !_server.IsListening) return;
|
if (_httpsServer == null || !_httpsServer.IsListening) return;
|
||||||
|
|
||||||
var message = logEvent.RenderMessage(_formatProvider);
|
var message = logEvent.RenderMessage(_formatProvider);
|
||||||
_server.WebSocketServices.Broadcast(message);
|
_httpsServer.WebSocketServices.Broadcast(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StartServerAndSetPort(int port)
|
public void StartServerAndSetPort(int port)
|
||||||
{
|
{
|
||||||
Debug.Console(0, "Starting Websocket Server on port: {0}", port);
|
Debug.Console(0, "Starting Websocket Server on port: {0}", port);
|
||||||
_server = new HttpServer(port);
|
|
||||||
_server.AddWebSocketService<DebugClient>(_path);
|
|
||||||
_server.Start();
|
Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword, @"\html\wss");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start(int port, string certPath = "", string certPassword = "", string rootPath = @"\html")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_httpsServer = new HttpServer(port, true)
|
||||||
|
{
|
||||||
|
RootPath = rootPath
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(certPath))
|
||||||
|
{
|
||||||
|
Debug.Console(0, "Assigning SSL Configuration");
|
||||||
|
_httpsServer.SslConfiguration = new ServerSslConfiguration(new X509Certificate2(certPath, certPassword))
|
||||||
|
{
|
||||||
|
ClientCertificateRequired = false,
|
||||||
|
CheckCertificateRevocation = false,
|
||||||
|
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls,
|
||||||
|
//this is just to test, you might want to actually validate
|
||||||
|
ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||||
|
{
|
||||||
|
Debug.Console(0, "HTTPS ClientCerticateValidation Callback triggered");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Debug.Console(0, "Adding Debug Client Service");
|
||||||
|
_httpsServer.AddWebSocketService<DebugClient>("/echo");
|
||||||
|
Debug.Console(0, "Assigning Log Info");
|
||||||
|
_httpsServer.Log.Level = LogLevel.Trace;
|
||||||
|
_httpsServer.Log.Output = delegate
|
||||||
|
{
|
||||||
|
//Debug.Print(DebugLevel.WebSocket, "{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s);
|
||||||
|
};
|
||||||
|
Debug.Console(0, "Starting");
|
||||||
|
|
||||||
|
//_httpsServer.OnGet += (sender, e) =>
|
||||||
|
//{
|
||||||
|
// Debug.Console(0, $"OnGet requesting {e.Request}");
|
||||||
|
// var req = e.Request;
|
||||||
|
// var res = e.Response;
|
||||||
|
|
||||||
|
// var path = req.RawUrl;
|
||||||
|
|
||||||
|
// if (path == "/")
|
||||||
|
// path += "index.html";
|
||||||
|
|
||||||
|
// var localPath = Path.Combine(rootPath, path.Substring(1));
|
||||||
|
|
||||||
|
// byte[] contents;
|
||||||
|
// if (File.Exists(localPath))
|
||||||
|
// contents = File.ReadAllBytes(localPath);
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// e.Response.StatusCode = 404;
|
||||||
|
// contents = Encoding.UTF8.GetBytes("Path not found " + e.Request.RawUrl);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var extention = Path.GetExtension(path);
|
||||||
|
// if (!_contentTypes.TryGetValue(extention, out var contentType))
|
||||||
|
// contentType = "text/html";
|
||||||
|
|
||||||
|
// res.ContentLength64 = contents.LongLength;
|
||||||
|
|
||||||
|
// res.Close(contents, true);
|
||||||
|
//};
|
||||||
|
|
||||||
|
_httpsServer.Start();
|
||||||
|
Debug.Console(0, "Ready");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.Console(0, "WebSocket Failed to start {0}", ex.Message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StopServer()
|
public void StopServer()
|
||||||
{
|
{
|
||||||
Debug.Console(0, "Stopping Websocket Server");
|
Debug.Console(0, "Stopping Websocket Server");
|
||||||
_server.Stop();
|
_httpsServer?.Stop();
|
||||||
|
|
||||||
|
_httpsServer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,39 +226,7 @@ namespace PepperDash.Core
|
|||||||
var url = Context.WebSocket.Url;
|
var url = Context.WebSocket.Url;
|
||||||
Debug.Console(2, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url);
|
Debug.Console(2, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url);
|
||||||
|
|
||||||
//var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)");
|
|
||||||
|
|
||||||
//if (match.Success)
|
|
||||||
//{
|
|
||||||
// var clientId = match.Groups[1].Value;
|
|
||||||
|
|
||||||
// // Inform controller of client joining
|
|
||||||
// if (Controller != null)
|
|
||||||
// {
|
|
||||||
// var clientJoined = new MobileControlResponseMessage
|
|
||||||
// {
|
|
||||||
// Type = "/system/roomKey",
|
|
||||||
// ClientId = clientId,
|
|
||||||
// Content = RoomKey,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// Controller.SendMessageObjectToDirectClient(clientJoined);
|
|
||||||
|
|
||||||
// var bridge = Controller.GetRoomBridge(RoomKey);
|
|
||||||
|
|
||||||
// SendUserCodeToClient(bridge, clientId);
|
|
||||||
|
|
||||||
// bridge.UserCodeChanged += (sender, args) => SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, clientId);
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// Debug.Console(2, "WebSocket UiClient Controller is null");
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
_connectionTime = DateTime.Now;
|
_connectionTime = DateTime.Now;
|
||||||
|
|
||||||
// TODO: Future: Check token to see if there's already an open session using that token and reject/close the session
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnMessage(MessageEventArgs e)
|
protected override void OnMessage(MessageEventArgs e)
|
||||||
@@ -165,7 +244,7 @@ namespace PepperDash.Core
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnError(ErrorEventArgs e)
|
protected override void OnError(WebSocketSharp.ErrorEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnError(e);
|
base.OnError(e);
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BouncyCastle" Version="1.8.9" />
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.19.35" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.19.35" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2">
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.2">
|
||||||
<Aliases>Full</Aliases>
|
<Aliases>Full</Aliases>
|
||||||
|
|||||||
360
src/Pepperdash Core/Web/BouncyCertificate.cs
Normal file
360
src/Pepperdash Core/Web/BouncyCertificate.cs
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
using Crestron.SimplSharp;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Org.BouncyCastle.Asn1;
|
||||||
|
using Org.BouncyCastle.Asn1.X509;
|
||||||
|
using Org.BouncyCastle.Crypto;
|
||||||
|
using Org.BouncyCastle.Crypto.Generators;
|
||||||
|
using Org.BouncyCastle.Crypto.Prng;
|
||||||
|
using Org.BouncyCastle.Math;
|
||||||
|
using Org.BouncyCastle.Pkcs;
|
||||||
|
using Org.BouncyCastle.Security;
|
||||||
|
using Org.BouncyCastle.Utilities;
|
||||||
|
using Org.BouncyCastle.X509;
|
||||||
|
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
|
||||||
|
using X509KeyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags;
|
||||||
|
using X509ContentType = System.Security.Cryptography.X509Certificates.X509ContentType;
|
||||||
|
using System.Text;
|
||||||
|
using Org.BouncyCastle.Crypto.Operators;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using BigInteger = Org.BouncyCastle.Math.BigInteger;
|
||||||
|
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
|
||||||
|
|
||||||
|
namespace PepperDash.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Taken From https://github.com/rlipscombe/bouncy-castle-csharp/
|
||||||
|
/// </summary>
|
||||||
|
internal class BouncyCertificate
|
||||||
|
{
|
||||||
|
public string CertificatePassword { get; set; } = "password";
|
||||||
|
public X509Certificate2 LoadCertificate(string issuerFileName, string password)
|
||||||
|
{
|
||||||
|
// We need to pass 'Exportable', otherwise we can't get the private key.
|
||||||
|
var issuerCertificate = new X509Certificate2(issuerFileName, password, X509KeyStorageFlags.Exportable);
|
||||||
|
return issuerCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate2 IssueCertificate(string subjectName, X509Certificate2 issuerCertificate, string[] subjectAlternativeNames, KeyPurposeID[] usages)
|
||||||
|
{
|
||||||
|
// It's self-signed, so these are the same.
|
||||||
|
var issuerName = issuerCertificate.Subject;
|
||||||
|
|
||||||
|
var random = GetSecureRandom();
|
||||||
|
var subjectKeyPair = GenerateKeyPair(random, 2048);
|
||||||
|
|
||||||
|
var issuerKeyPair = DotNetUtilities.GetKeyPair(issuerCertificate.PrivateKey);
|
||||||
|
|
||||||
|
var serialNumber = GenerateSerialNumber(random);
|
||||||
|
var issuerSerialNumber = new BigInteger(issuerCertificate.GetSerialNumber());
|
||||||
|
|
||||||
|
const bool isCertificateAuthority = false;
|
||||||
|
var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
|
||||||
|
subjectAlternativeNames, issuerName, issuerKeyPair,
|
||||||
|
issuerSerialNumber, isCertificateAuthority,
|
||||||
|
usages);
|
||||||
|
return ConvertCertificate(certificate, subjectKeyPair, random);
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate2 CreateCertificateAuthorityCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages)
|
||||||
|
{
|
||||||
|
// It's self-signed, so these are the same.
|
||||||
|
var issuerName = subjectName;
|
||||||
|
|
||||||
|
var random = GetSecureRandom();
|
||||||
|
var subjectKeyPair = GenerateKeyPair(random, 2048);
|
||||||
|
|
||||||
|
// It's self-signed, so these are the same.
|
||||||
|
var issuerKeyPair = subjectKeyPair;
|
||||||
|
|
||||||
|
var serialNumber = GenerateSerialNumber(random);
|
||||||
|
var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number.
|
||||||
|
|
||||||
|
const bool isCertificateAuthority = true;
|
||||||
|
var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
|
||||||
|
subjectAlternativeNames, issuerName, issuerKeyPair,
|
||||||
|
issuerSerialNumber, isCertificateAuthority,
|
||||||
|
usages);
|
||||||
|
return ConvertCertificate(certificate, subjectKeyPair, random);
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate2 CreateSelfSignedCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages)
|
||||||
|
{
|
||||||
|
// It's self-signed, so these are the same.
|
||||||
|
var issuerName = subjectName;
|
||||||
|
|
||||||
|
var random = GetSecureRandom();
|
||||||
|
var subjectKeyPair = GenerateKeyPair(random, 2048);
|
||||||
|
|
||||||
|
// It's self-signed, so these are the same.
|
||||||
|
var issuerKeyPair = subjectKeyPair;
|
||||||
|
|
||||||
|
var serialNumber = GenerateSerialNumber(random);
|
||||||
|
var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number.
|
||||||
|
|
||||||
|
const bool isCertificateAuthority = false;
|
||||||
|
var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
|
||||||
|
subjectAlternativeNames, issuerName, issuerKeyPair,
|
||||||
|
issuerSerialNumber, isCertificateAuthority,
|
||||||
|
usages);
|
||||||
|
return ConvertCertificate(certificate, subjectKeyPair, random);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SecureRandom GetSecureRandom()
|
||||||
|
{
|
||||||
|
// Since we're on Windows, we'll use the CryptoAPI one (on the assumption
|
||||||
|
// that it might have access to better sources of entropy than the built-in
|
||||||
|
// Bouncy Castle ones):
|
||||||
|
var randomGenerator = new CryptoApiRandomGenerator();
|
||||||
|
var random = new SecureRandom(randomGenerator);
|
||||||
|
return random;
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509Certificate GenerateCertificate(SecureRandom random,
|
||||||
|
string subjectName,
|
||||||
|
AsymmetricCipherKeyPair subjectKeyPair,
|
||||||
|
BigInteger subjectSerialNumber,
|
||||||
|
string[] subjectAlternativeNames,
|
||||||
|
string issuerName,
|
||||||
|
AsymmetricCipherKeyPair issuerKeyPair,
|
||||||
|
BigInteger issuerSerialNumber,
|
||||||
|
bool isCertificateAuthority,
|
||||||
|
KeyPurposeID[] usages)
|
||||||
|
{
|
||||||
|
var certificateGenerator = new X509V3CertificateGenerator();
|
||||||
|
|
||||||
|
certificateGenerator.SetSerialNumber(subjectSerialNumber);
|
||||||
|
|
||||||
|
var issuerDN = new X509Name(issuerName);
|
||||||
|
certificateGenerator.SetIssuerDN(issuerDN);
|
||||||
|
|
||||||
|
// Note: The subject can be omitted if you specify a subject alternative name (SAN).
|
||||||
|
var subjectDN = new X509Name(subjectName);
|
||||||
|
certificateGenerator.SetSubjectDN(subjectDN);
|
||||||
|
|
||||||
|
// Our certificate needs valid from/to values.
|
||||||
|
var notBefore = DateTime.UtcNow.Date;
|
||||||
|
var notAfter = notBefore.AddYears(2);
|
||||||
|
|
||||||
|
certificateGenerator.SetNotBefore(notBefore);
|
||||||
|
certificateGenerator.SetNotAfter(notAfter);
|
||||||
|
|
||||||
|
// The subject's public key goes in the certificate.
|
||||||
|
certificateGenerator.SetPublicKey(subjectKeyPair.Public);
|
||||||
|
|
||||||
|
AddAuthorityKeyIdentifier(certificateGenerator, issuerDN, issuerKeyPair, issuerSerialNumber);
|
||||||
|
AddSubjectKeyIdentifier(certificateGenerator, subjectKeyPair);
|
||||||
|
//AddBasicConstraints(certificateGenerator, isCertificateAuthority);
|
||||||
|
|
||||||
|
if (usages != null && usages.Any())
|
||||||
|
AddExtendedKeyUsage(certificateGenerator, usages);
|
||||||
|
|
||||||
|
if (subjectAlternativeNames != null && subjectAlternativeNames.Any())
|
||||||
|
AddSubjectAlternativeNames(certificateGenerator, subjectAlternativeNames);
|
||||||
|
|
||||||
|
// Set the signature algorithm. This is used to generate the thumbprint which is then signed
|
||||||
|
// with the issuer's private key. We'll use SHA-256, which is (currently) considered fairly strong.
|
||||||
|
const string signatureAlgorithm = "SHA256WithRSA";
|
||||||
|
|
||||||
|
// The certificate is signed with the issuer's private key.
|
||||||
|
ISignatureFactory signatureFactory = new Asn1SignatureFactory(signatureAlgorithm, issuerKeyPair.Private, random);
|
||||||
|
var certificate = certificateGenerator.Generate(signatureFactory);
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The certificate needs a serial number. This is used for revocation,
|
||||||
|
/// and usually should be an incrementing index (which makes it easier to revoke a range of certificates).
|
||||||
|
/// Since we don't have anywhere to store the incrementing index, we can just use a random number.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="random"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private BigInteger GenerateSerialNumber(SecureRandom random)
|
||||||
|
{
|
||||||
|
var serialNumber =
|
||||||
|
BigIntegers.CreateRandomInRange(
|
||||||
|
BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
|
||||||
|
return serialNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a key pair.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="random">The random number generator.</param>
|
||||||
|
/// <param name="strength">The key length in bits. For RSA, 2048 bits should be considered the minimum acceptable these days.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private AsymmetricCipherKeyPair GenerateKeyPair(SecureRandom random, int strength)
|
||||||
|
{
|
||||||
|
var keyGenerationParameters = new KeyGenerationParameters(random, strength);
|
||||||
|
|
||||||
|
var keyPairGenerator = new RsaKeyPairGenerator();
|
||||||
|
keyPairGenerator.Init(keyGenerationParameters);
|
||||||
|
var subjectKeyPair = keyPairGenerator.GenerateKeyPair();
|
||||||
|
return subjectKeyPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add the Authority Key Identifier. According to http://www.alvestrand.no/objectid/2.5.29.35.html, this
|
||||||
|
/// identifies the public key to be used to verify the signature on this certificate.
|
||||||
|
/// In a certificate chain, this corresponds to the "Subject Key Identifier" on the *issuer* certificate.
|
||||||
|
/// The Bouncy Castle documentation, at http://www.bouncycastle.org/wiki/display/JA1/X.509+Public+Key+Certificate+and+Certification+Request+Generation,
|
||||||
|
/// shows how to create this from the issuing certificate. Since we're creating a self-signed certificate, we have to do this slightly differently.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="certificateGenerator"></param>
|
||||||
|
/// <param name="issuerDN"></param>
|
||||||
|
/// <param name="issuerKeyPair"></param>
|
||||||
|
/// <param name="issuerSerialNumber"></param>
|
||||||
|
private void AddAuthorityKeyIdentifier(X509V3CertificateGenerator certificateGenerator,
|
||||||
|
X509Name issuerDN,
|
||||||
|
AsymmetricCipherKeyPair issuerKeyPair,
|
||||||
|
BigInteger issuerSerialNumber)
|
||||||
|
{
|
||||||
|
var authorityKeyIdentifierExtension =
|
||||||
|
new AuthorityKeyIdentifier(
|
||||||
|
SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(issuerKeyPair.Public),
|
||||||
|
new GeneralNames(new GeneralName(issuerDN)),
|
||||||
|
issuerSerialNumber);
|
||||||
|
certificateGenerator.AddExtension(
|
||||||
|
X509Extensions.AuthorityKeyIdentifier.Id, false, authorityKeyIdentifierExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add the "Subject Alternative Names" extension. Note that you have to repeat
|
||||||
|
/// the value from the "Subject Name" property.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="certificateGenerator"></param>
|
||||||
|
/// <param name="subjectAlternativeNames"></param>
|
||||||
|
private void AddSubjectAlternativeNames(X509V3CertificateGenerator certificateGenerator,
|
||||||
|
IEnumerable<string> subjectAlternativeNames)
|
||||||
|
{
|
||||||
|
var subjectAlternativeNamesExtension =
|
||||||
|
new DerSequence(
|
||||||
|
subjectAlternativeNames.Select(name => new GeneralName(GeneralName.DnsName, name))
|
||||||
|
.ToArray<Asn1Encodable>());
|
||||||
|
certificateGenerator.AddExtension(
|
||||||
|
X509Extensions.SubjectAlternativeName.Id, false, subjectAlternativeNamesExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add the "Extended Key Usage" extension, specifying (for example) "server authentication".
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="certificateGenerator"></param>
|
||||||
|
/// <param name="usages"></param>
|
||||||
|
private void AddExtendedKeyUsage(X509V3CertificateGenerator certificateGenerator, KeyPurposeID[] usages)
|
||||||
|
{
|
||||||
|
certificateGenerator.AddExtension(
|
||||||
|
X509Extensions.ExtendedKeyUsage.Id, false, new ExtendedKeyUsage(usages));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add the "Basic Constraints" extension.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="certificateGenerator"></param>
|
||||||
|
/// <param name="isCertificateAuthority"></param>
|
||||||
|
private void AddBasicConstraints(X509V3CertificateGenerator certificateGenerator,
|
||||||
|
bool isCertificateAuthority)
|
||||||
|
{
|
||||||
|
certificateGenerator.AddExtension(
|
||||||
|
X509Extensions.BasicConstraints.Id, true, new BasicConstraints(isCertificateAuthority));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add the Subject Key Identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="certificateGenerator"></param>
|
||||||
|
/// <param name="subjectKeyPair"></param>
|
||||||
|
private void AddSubjectKeyIdentifier(X509V3CertificateGenerator certificateGenerator,
|
||||||
|
AsymmetricCipherKeyPair subjectKeyPair)
|
||||||
|
{
|
||||||
|
var subjectKeyIdentifierExtension =
|
||||||
|
new SubjectKeyIdentifier(
|
||||||
|
SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(subjectKeyPair.Public));
|
||||||
|
certificateGenerator.AddExtension(
|
||||||
|
X509Extensions.SubjectKeyIdentifier.Id, false, subjectKeyIdentifierExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509Certificate2 ConvertCertificate(X509Certificate certificate,
|
||||||
|
AsymmetricCipherKeyPair subjectKeyPair,
|
||||||
|
SecureRandom random)
|
||||||
|
{
|
||||||
|
// Now to convert the Bouncy Castle certificate to a .NET certificate.
|
||||||
|
// See http://web.archive.org/web/20100504192226/http://www.fkollmann.de/v2/post/Creating-certificates-using-BouncyCastle.aspx
|
||||||
|
// ...but, basically, we create a PKCS12 store (a .PFX file) in memory, and add the public and private key to that.
|
||||||
|
var store = new Pkcs12Store();
|
||||||
|
|
||||||
|
// What Bouncy Castle calls "alias" is the same as what Windows terms the "friendly name".
|
||||||
|
string friendlyName = certificate.SubjectDN.ToString();
|
||||||
|
|
||||||
|
// Add the certificate.
|
||||||
|
var certificateEntry = new X509CertificateEntry(certificate);
|
||||||
|
store.SetCertificateEntry(friendlyName, certificateEntry);
|
||||||
|
|
||||||
|
// Add the private key.
|
||||||
|
store.SetKeyEntry(friendlyName, new AsymmetricKeyEntry(subjectKeyPair.Private), new[] { certificateEntry });
|
||||||
|
|
||||||
|
// Convert it to an X509Certificate2 object by saving/loading it from a MemoryStream.
|
||||||
|
// It needs a password. Since we'll remove this later, it doesn't particularly matter what we use.
|
||||||
|
|
||||||
|
var stream = new MemoryStream();
|
||||||
|
store.Save(stream, CertificatePassword.ToCharArray(), random);
|
||||||
|
|
||||||
|
var convertedCertificate =
|
||||||
|
new X509Certificate2(stream.ToArray(),
|
||||||
|
CertificatePassword,
|
||||||
|
X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
|
||||||
|
return convertedCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteCertificate(X509Certificate2 certificate, string outputDirectory, string certName)
|
||||||
|
{
|
||||||
|
// This password is the one attached to the PFX file. Use 'null' for no password.
|
||||||
|
// Create PFX (PKCS #12) with private key
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pfx = certificate.Export(X509ContentType.Pfx, CertificatePassword);
|
||||||
|
File.WriteAllBytes($"{Path.Combine(outputDirectory, certName)}.pfx", pfx);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
CrestronConsole.PrintLine($"Failed to write x509 cert pfx\r\n{ex.Message}");
|
||||||
|
}
|
||||||
|
// Create Base 64 encoded CER (public key only)
|
||||||
|
using (var writer = new StreamWriter($"{Path.Combine(outputDirectory, certName)}.cer", false))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var contents = $"-----BEGIN CERTIFICATE-----\r\n{Convert.ToBase64String(certificate.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)}\r\n-----END CERTIFICATE-----";
|
||||||
|
writer.Write(contents);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
CrestronConsole.PrintLine($"Failed to write x509 cert cer\r\n{ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool AddCertToStore(X509Certificate2 cert, System.Security.Cryptography.X509Certificates.StoreName st, System.Security.Cryptography.X509Certificates.StoreLocation sl)
|
||||||
|
{
|
||||||
|
bool bRet = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var store = new System.Security.Cryptography.X509Certificates.X509Store(st, sl);
|
||||||
|
store.Open(System.Security.Cryptography.X509Certificates.OpenFlags.ReadWrite);
|
||||||
|
store.Add(cert);
|
||||||
|
|
||||||
|
store.Close();
|
||||||
|
bRet = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
CrestronConsole.PrintLine($"AddCertToStore Failed\r\n{ex.Message}\r\n{ex.StackTrace}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return bRet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user