From 591824f3cc719e1afb3b603cc71391ed819c96bb Mon Sep 17 00:00:00 2001 From: Archangel Date: Thu, 20 Feb 2025 15:02:25 +0100 Subject: [PATCH] Start refactor for certificate loading --- Libraries/Core/Helpers/CertificateHelper.cs | 174 ++++++++++++++++++++ Libraries/Core/Servers/HttpServer.cs | 84 +--------- 2 files changed, 177 insertions(+), 81 deletions(-) create mode 100644 Libraries/Core/Helpers/CertificateHelper.cs diff --git a/Libraries/Core/Helpers/CertificateHelper.cs b/Libraries/Core/Helpers/CertificateHelper.cs new file mode 100644 index 00000000..825b4b33 --- /dev/null +++ b/Libraries/Core/Helpers/CertificateHelper.cs @@ -0,0 +1,174 @@ +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography; +using SptCommon.Annotations; +using Core.Models.Utils; +using Core.Utils; + +namespace Core.Helpers +{ + [Injectable] + public class CertificateHelper(ISptLogger _logger, FileUtil _fileUtil) + { + private const string certificatePath = "./user/certs/server.crt"; + private const string certificateKeyPath = "./user/certs/server.key"; + private const string certificatePfxPath = "./user/certs/certificate.pfx"; + + //Todo: Finish off to match TS server + public X509Certificate2 LoadOrGenerateCertificate() + { + if (!Directory.Exists("./user/certs")) + { + Directory.CreateDirectory("./user/certs"); + } + + var certificate = LoadCertificate(); + + if (certificate == null) + { + // Generate self-signed certificate + certificate = GenerateSelfSignedCertificate("localhost"); + SaveCertificate(certificate); // Save cert and new key + + _logger.Success($"Generated and stored self-signed certificate ({certificatePath})"); + } + + return certificate; + } + + //Todo: When the above is finished off, remove any method with Pfx in the name + public X509Certificate2 LoadOrGenerateCertificatePfx() + { + if (!Directory.Exists("./user/certs")) + { + Directory.CreateDirectory("./user/certs"); + } + + var certificate = LoadCertificatePfx(); + + if (certificate == null) + { + // Generate self-signed certificate + certificate = GenerateSelfSignedCertificate("localhost"); + SaveCertificatePfx(certificate); // Save cert + + _logger.Success($"Generated and stored self-signed certificate ({certificatePath})"); + } + + return certificate; + } + + private X509Certificate2? LoadCertificate() + { + try + { + return X509Certificate2.CreateFromPemFile(certificatePath, certificateKeyPath); + } + catch (Exception) + { + return null; + } + } + + /// + /// Get a certificate from provided path and return + /// + /// Path to pfx file + /// Optional password for certificate + /// X509Certificate2 + private X509Certificate2? LoadCertificatePfx() + { + try + { + //Archangel: For some reason despite this being deprecated this is the only way to load a certificate file + //No idea why, I want to eventually switch over to the other format so it lines up with the TS server + //But for now this works fine + return new(certificatePfxPath); + } + catch (Exception) + { + return null; + } + } + + /// + /// Generate and return a self-signed certificate + /// + /// e.g. localhost + /// X509Certificate2 + private X509Certificate2 GenerateSelfSignedCertificate(string subjectName) + { + var sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddIpAddress(IPAddress.Loopback); + sanBuilder.AddDnsName("localhost"); + sanBuilder.AddDnsName(Environment.MachineName); + + var distinguishedName = new X500DistinguishedName($"CN={subjectName}"); + + using var rsa = RSA.Create(2048); + var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + request.CertificateExtensions.Add(sanBuilder.Build()); + + //Todo: Enable when Pfx methods can be removed + //SavePrivateKey(rsa); + + return request.CreateSelfSigned(new DateTimeOffset(DateTime.UtcNow.AddDays(-1)), new DateTimeOffset(DateTime.UtcNow.AddDays(3650))); + } + + /// + /// Save a certificate as a file to disk + /// + /// Certificate to save + private void SaveCertificate(X509Certificate2 certificate) + { + try + { + // Save as PEM (ensure the certificate is in PEM format) + var certPem = "-----BEGIN CERTIFICATE-----\n" + + Convert.ToBase64String(certificate.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks) + + "\n-----END CERTIFICATE-----"; + _fileUtil.WriteFile(certificatePath, certPem); + } + catch (Exception ex) + { + _logger.Error($"Error saving certificate: {ex.Message}"); + } + } + + /// + /// Save a certificate as a file to disk + /// + /// Certificate to save + private void SaveCertificatePfx(X509Certificate2 certificate) + { + try + { + _fileUtil.WriteFile(certificatePfxPath, certificate.Export(X509ContentType.Pfx)); + } + catch (Exception ex) + { + _logger.Error($"Error saving certificate: {ex.Message}"); + } + } + + private void SavePrivateKey(RSA privateKey) + { + try + { + var privateKeyBytes = privateKey.ExportPkcs8PrivateKey(); + + // Convert the private key to PEM format (Base64 encoded) + var privateKeyString = "-----BEGIN PRIVATE KEY-----\n" + + Convert.ToBase64String(privateKeyBytes, Base64FormattingOptions.InsertLineBreaks) + + "\n-----END PRIVATE KEY-----"; + + _fileUtil.WriteFile(certificateKeyPath, privateKeyString); + } + catch (Exception ex) + { + _logger.Error($"Error saving certificate key: {ex.Message}"); + } + } + + } +} diff --git a/Libraries/Core/Servers/HttpServer.cs b/Libraries/Core/Servers/HttpServer.cs index 4198eb31..13e040ef 100644 --- a/Libraries/Core/Servers/HttpServer.cs +++ b/Libraries/Core/Servers/HttpServer.cs @@ -3,6 +3,7 @@ using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Core.Context; +using Core.Helpers; using Core.Models.Spt.Config; using Core.Models.Utils; using Core.Servers.Http; @@ -19,6 +20,7 @@ public class HttpServer( ISptLogger _logger, LocalisationService _localisationService, ConfigServer _configServer, + CertificateHelper _certificateHelper, ApplicationContext _applicationContext, WebSocketServer _webSocketServer, IEnumerable _httpListeners @@ -40,24 +42,13 @@ public class HttpServer( builder.WebHost.ConfigureKestrel( options => { - const string certFileName = "certificate.pfx"; - var certificate = LoadCertificate(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, certFileName, _httpConfig.CertificatePassword)); - if (certificate == null) - { - // Generate self-signed certificate - certificate = GenerateSelfSignedCertificate("localhost"); - SaveCertificate(certificate, certFileName); // Save cert - - _logger.Success($"Generated and stored self-signed certificate ({certFileName}) in {AppDomain.CurrentDomain.BaseDirectory}"); - } - options.ListenAnyIP(_httpConfig.Port, listenOptions => { listenOptions.UseHttps(opts => { opts.SslProtocols = SslProtocols.Tls12; opts.AllowAnyClientCertificate(); - opts.ServerCertificate = certificate; + opts.ServerCertificate = _certificateHelper.LoadOrGenerateCertificatePfx(); opts.ClientCertificateMode = ClientCertificateMode.NoCertificate; }); }); @@ -84,75 +75,6 @@ public class HttpServer( _applicationContext.AddValue(ContextVariableType.WEB_APPLICATION, app); } - /// - /// Get a certificate from provided path and return - /// - /// Path to pfx file - /// Optional password for certificate - /// X509Certificate2 - private X509Certificate2? LoadCertificate(string pfxPath, string? certPassword = null) - { - if (File.Exists(pfxPath)) - { - try - { - //TODO: use this - //return X509CertificateLoader.LoadCertificateFromFile(pfxPath); - return string.IsNullOrEmpty(certPassword) - ? new X509Certificate2(pfxPath) - : new X509Certificate2(pfxPath, certPassword); - } - catch (Exception ex) - { - _logger.Error($"Error loading certificate from path: {pfxPath} error: {ex.Message}"); - - return null; - } - } - - return null; - } - - /// - /// Generate and return a self-signed certificate - /// - /// e.g. localhost - /// X509Certificate2 - private X509Certificate2 GenerateSelfSignedCertificate(string subjectName) - { - var sanBuilder = new SubjectAlternativeNameBuilder(); - sanBuilder.AddIpAddress(IPAddress.Loopback); - sanBuilder.AddIpAddress(IPAddress.IPv6Loopback); - sanBuilder.AddIpAddress(new IPAddress(new byte[] { 127, 0, 0, 1 })); - sanBuilder.AddDnsName("localhost"); - sanBuilder.AddDnsName(Environment.MachineName); - - var distinguishedName = new X500DistinguishedName($"CN={subjectName}"); - - using var rsa = RSA.Create(2048); - var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - request.CertificateExtensions.Add(sanBuilder.Build()); - - return request.CreateSelfSigned(new DateTimeOffset(DateTime.UtcNow.AddDays(-1)), new DateTimeOffset(DateTime.UtcNow.AddDays(3650))); - } - - /// - /// Save a certificate as a file to disk - /// - /// Certificate to save - /// Path to destination - private void SaveCertificate(X509Certificate2 certificate, string pfxPath) - { - try - { - File.WriteAllBytes(pfxPath, certificate.Export(X509ContentType.Pfx)); - } - catch (Exception ex) - { - _logger.Error($"Error saving certificate: {ex.Message}"); - } - } - private async Task HandleFallback(HttpContext context) { if (context.WebSockets.IsWebSocketRequest)