deprecate(lmxproxy): move all LmxProxy code, tests, and docs to deprecated/
LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL adapter files, and related docs to deprecated/. Removed LmxProxy registration from DataConnectionFactory, project reference from DCL, protocol option from UI, and cleaned up all requirement docs.
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.LmxProxy.Host.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages TLS certificates for the LmxProxy service, including generation and validation
|
||||
/// </summary>
|
||||
public class TlsCertificateManager
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<TlsCertificateManager>();
|
||||
private readonly TlsConfiguration _tlsConfiguration;
|
||||
|
||||
public TlsCertificateManager(TlsConfiguration tlsConfiguration)
|
||||
{
|
||||
_tlsConfiguration = tlsConfiguration ?? throw new ArgumentNullException(nameof(tlsConfiguration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks TLS certificate status and creates new certificates if needed
|
||||
/// </summary>
|
||||
/// <returns>True if certificates are valid or were successfully created</returns>
|
||||
public bool EnsureCertificatesValid()
|
||||
{
|
||||
if (!_tlsConfiguration.Enabled)
|
||||
{
|
||||
Logger.Information("TLS is disabled, skipping certificate check");
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Check if certificate files exist
|
||||
bool certificateExists = File.Exists(_tlsConfiguration.ServerCertificatePath);
|
||||
bool keyExists = File.Exists(_tlsConfiguration.ServerKeyPath);
|
||||
|
||||
if (!certificateExists || !keyExists)
|
||||
{
|
||||
Logger.Warning("TLS certificate or key not found, generating new certificate");
|
||||
return GenerateNewCertificate();
|
||||
}
|
||||
|
||||
// Check certificate expiration
|
||||
if (IsCertificateExpiringSoon(_tlsConfiguration.ServerCertificatePath))
|
||||
{
|
||||
Logger.Warning("TLS certificate is expiring within the next year, generating new certificate");
|
||||
return GenerateNewCertificate();
|
||||
}
|
||||
|
||||
Logger.Information("TLS certificate is valid");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "Error checking TLS certificates");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a certificate is expiring within the next year
|
||||
/// </summary>
|
||||
private bool IsCertificateExpiringSoon(string certificatePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
string certPem = File.ReadAllText(certificatePath);
|
||||
byte[] certBytes = GetBytesFromPem(certPem, "CERTIFICATE");
|
||||
|
||||
using var cert = new X509Certificate2(certBytes);
|
||||
DateTime expirationDate = cert.NotAfter;
|
||||
double daysUntilExpiration = (expirationDate - DateTime.Now).TotalDays;
|
||||
|
||||
Logger.Information("Certificate expires on {ExpirationDate} ({DaysUntilExpiration:F0} days from now)",
|
||||
expirationDate, daysUntilExpiration);
|
||||
|
||||
// Check if expiring within the next year (365 days)
|
||||
return daysUntilExpiration <= 365;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "Error checking certificate expiration");
|
||||
// If we can't check expiration, assume it needs renewal
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new self-signed certificate
|
||||
/// </summary>
|
||||
private bool GenerateNewCertificate()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Information("Generating new self-signed TLS certificate");
|
||||
|
||||
// Ensure directory exists
|
||||
string? certDir = Path.GetDirectoryName(_tlsConfiguration.ServerCertificatePath);
|
||||
if (!string.IsNullOrEmpty(certDir) && !Directory.Exists(certDir))
|
||||
{
|
||||
Directory.CreateDirectory(certDir);
|
||||
Logger.Information("Created certificate directory: {Directory}", certDir);
|
||||
}
|
||||
|
||||
// Generate a new self-signed certificate
|
||||
using var rsa = RSA.Create(2048);
|
||||
var request = new CertificateRequest(
|
||||
"CN=LmxProxy, O=SCADA Bridge, C=US",
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
|
||||
// Add certificate extensions
|
||||
request.CertificateExtensions.Add(
|
||||
new X509BasicConstraintsExtension(false, false, 0, false));
|
||||
|
||||
request.CertificateExtensions.Add(
|
||||
new X509KeyUsageExtension(
|
||||
X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,
|
||||
false));
|
||||
|
||||
request.CertificateExtensions.Add(
|
||||
new X509EnhancedKeyUsageExtension(
|
||||
new OidCollection
|
||||
{
|
||||
new Oid("1.3.6.1.5.5.7.3.1") // Server Authentication
|
||||
},
|
||||
false));
|
||||
|
||||
// Add Subject Alternative Names
|
||||
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||
sanBuilder.AddDnsName("localhost");
|
||||
sanBuilder.AddDnsName(Environment.MachineName);
|
||||
sanBuilder.AddIpAddress(IPAddress.Loopback);
|
||||
sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);
|
||||
request.CertificateExtensions.Add(sanBuilder.Build());
|
||||
|
||||
// Create the certificate with 2-year validity
|
||||
DateTimeOffset notBefore = DateTimeOffset.Now.AddDays(-1);
|
||||
DateTimeOffset notAfter = DateTimeOffset.Now.AddYears(2);
|
||||
|
||||
using X509Certificate2? cert = request.CreateSelfSigned(notBefore, notAfter);
|
||||
|
||||
// Export certificate to PEM format
|
||||
string certPem = ExportCertificateToPem(cert);
|
||||
File.WriteAllText(_tlsConfiguration.ServerCertificatePath, certPem);
|
||||
Logger.Information("Saved certificate to {Path}", _tlsConfiguration.ServerCertificatePath);
|
||||
|
||||
// Export private key to PEM format
|
||||
string keyPem = ExportPrivateKeyToPem(rsa);
|
||||
File.WriteAllText(_tlsConfiguration.ServerKeyPath, keyPem);
|
||||
Logger.Information("Saved private key to {Path}", _tlsConfiguration.ServerKeyPath);
|
||||
|
||||
// If client CA path is specified and doesn't exist, create it
|
||||
if (!string.IsNullOrWhiteSpace(_tlsConfiguration.ClientCaCertificatePath) &&
|
||||
!File.Exists(_tlsConfiguration.ClientCaCertificatePath))
|
||||
{
|
||||
// For self-signed certificates, the CA cert is the same as the server cert
|
||||
File.WriteAllText(_tlsConfiguration.ClientCaCertificatePath, certPem);
|
||||
Logger.Information("Saved CA certificate to {Path}", _tlsConfiguration.ClientCaCertificatePath);
|
||||
}
|
||||
|
||||
Logger.Information("Successfully generated new TLS certificate valid until {NotAfter}", notAfter);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "Failed to generate new TLS certificate");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports a certificate to PEM format
|
||||
/// </summary>
|
||||
private static string ExportCertificateToPem(X509Certificate2 cert)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("-----BEGIN CERTIFICATE-----");
|
||||
builder.AppendLine(Convert.ToBase64String(cert.Export(X509ContentType.Cert),
|
||||
Base64FormattingOptions.InsertLineBreaks));
|
||||
builder.AppendLine("-----END CERTIFICATE-----");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports an RSA private key to PEM format
|
||||
/// </summary>
|
||||
private static string ExportPrivateKeyToPem(RSA rsa)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("-----BEGIN RSA PRIVATE KEY-----");
|
||||
|
||||
// For .NET Framework 4.8, we need to use the older export method
|
||||
RSAParameters parameters = rsa.ExportParameters(true);
|
||||
byte[] keyBytes = EncodeRSAPrivateKey(parameters);
|
||||
builder.AppendLine(Convert.ToBase64String(keyBytes, Base64FormattingOptions.InsertLineBreaks));
|
||||
|
||||
builder.AppendLine("-----END RSA PRIVATE KEY-----");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes RSA parameters to PKCS#1 format for .NET Framework 4.8
|
||||
/// </summary>
|
||||
private static byte[] EncodeRSAPrivateKey(RSAParameters parameters)
|
||||
{
|
||||
using (var stream = new MemoryStream())
|
||||
using (var writer = new BinaryWriter(stream))
|
||||
{
|
||||
// Write version
|
||||
writer.Write((byte)0x02); // INTEGER
|
||||
writer.Write((byte)0x01); // Length
|
||||
writer.Write((byte)0x00); // Version
|
||||
|
||||
// Write modulus
|
||||
WriteIntegerBytes(writer, parameters.Modulus);
|
||||
|
||||
// Write public exponent
|
||||
WriteIntegerBytes(writer, parameters.Exponent);
|
||||
|
||||
// Write private exponent
|
||||
WriteIntegerBytes(writer, parameters.D);
|
||||
|
||||
// Write prime1
|
||||
WriteIntegerBytes(writer, parameters.P);
|
||||
|
||||
// Write prime2
|
||||
WriteIntegerBytes(writer, parameters.Q);
|
||||
|
||||
// Write exponent1
|
||||
WriteIntegerBytes(writer, parameters.DP);
|
||||
|
||||
// Write exponent2
|
||||
WriteIntegerBytes(writer, parameters.DQ);
|
||||
|
||||
// Write coefficient
|
||||
WriteIntegerBytes(writer, parameters.InverseQ);
|
||||
|
||||
byte[] innerBytes = stream.ToArray();
|
||||
|
||||
// Create SEQUENCE wrapper
|
||||
using (var finalStream = new MemoryStream())
|
||||
using (var finalWriter = new BinaryWriter(finalStream))
|
||||
{
|
||||
finalWriter.Write((byte)0x30); // SEQUENCE
|
||||
WriteLength(finalWriter, innerBytes.Length);
|
||||
finalWriter.Write(innerBytes);
|
||||
return finalStream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteIntegerBytes(BinaryWriter writer, byte[] bytes)
|
||||
{
|
||||
if (bytes == null)
|
||||
{
|
||||
bytes = new byte[] { 0 };
|
||||
}
|
||||
|
||||
writer.Write((byte)0x02); // INTEGER
|
||||
|
||||
if (bytes[0] >= 0x80)
|
||||
{
|
||||
// Add padding byte for positive number
|
||||
WriteLength(writer, bytes.Length + 1);
|
||||
writer.Write((byte)0x00);
|
||||
writer.Write(bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteLength(writer, bytes.Length);
|
||||
writer.Write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteLength(BinaryWriter writer, int length)
|
||||
{
|
||||
if (length < 0x80)
|
||||
{
|
||||
writer.Write((byte)length);
|
||||
}
|
||||
else if (length <= 0xFF)
|
||||
{
|
||||
writer.Write((byte)0x81);
|
||||
writer.Write((byte)length);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.Write((byte)0x82);
|
||||
writer.Write((byte)(length >> 8));
|
||||
writer.Write((byte)(length & 0xFF));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts bytes from PEM format
|
||||
/// </summary>
|
||||
private static byte[] GetBytesFromPem(string pem, string section)
|
||||
{
|
||||
string header = $"-----BEGIN {section}-----";
|
||||
string footer = $"-----END {section}-----";
|
||||
|
||||
int start = pem.IndexOf(header, StringComparison.Ordinal);
|
||||
if (start < 0)
|
||||
{
|
||||
throw new InvalidOperationException($"PEM {section} header not found");
|
||||
}
|
||||
|
||||
start += header.Length;
|
||||
int end = pem.IndexOf(footer, start, StringComparison.Ordinal);
|
||||
|
||||
if (end < 0)
|
||||
{
|
||||
throw new InvalidOperationException($"PEM {section} footer not found");
|
||||
}
|
||||
|
||||
// Use Substring instead of range syntax for .NET Framework 4.8 compatibility
|
||||
string base64 = pem.Substring(start, end - start).Replace("\r", "").Replace("\n", "");
|
||||
return Convert.FromBase64String(base64);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user