Start refactor for certificate loading

This commit is contained in:
Archangel
2025-02-20 15:02:25 +01:00
parent 92c1f6502d
commit 591824f3cc
2 changed files with 177 additions and 81 deletions
+174
View File
@@ -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<CertificateHelper> _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;
}
}
/// <summary>
/// Get a certificate from provided path and return
/// </summary>
/// <param name="pfxPath">Path to pfx file</param>
/// <param name="certPassword">Optional password for certificate</param>
/// <returns>X509Certificate2</returns>
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;
}
}
/// <summary>
/// Generate and return a self-signed certificate
/// </summary>
/// <param name="subjectName">e.g. localhost</param>
/// <returns>X509Certificate2</returns>
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)));
}
/// <summary>
/// Save a certificate as a file to disk
/// </summary>
/// <param name="certificate">Certificate to save</param>
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}");
}
}
/// <summary>
/// Save a certificate as a file to disk
/// </summary>
/// <param name="certificate">Certificate to save</param>
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}");
}
}
}
}
+3 -81
View File
@@ -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<HttpServer> _logger,
LocalisationService _localisationService,
ConfigServer _configServer,
CertificateHelper _certificateHelper,
ApplicationContext _applicationContext,
WebSocketServer _webSocketServer,
IEnumerable<IHttpListener> _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);
}
/// <summary>
/// Get a certificate from provided path and return
/// </summary>
/// <param name="pfxPath">Path to pfx file</param>
/// <param name="certPassword">Optional password for certificate</param>
/// <returns>X509Certificate2</returns>
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;
}
/// <summary>
/// Generate and return a self-signed certificate
/// </summary>
/// <param name="subjectName">e.g. localhost</param>
/// <returns>X509Certificate2</returns>
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)));
}
/// <summary>
/// Save a certificate as a file to disk
/// </summary>
/// <param name="certificate">Certificate to save</param>
/// <param name="pfxPath">Path to destination</param>
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)