-
-
Notifications
You must be signed in to change notification settings - Fork 267
Description
I'm not sure if it was just my copy or this is everywhere, but just wanted to give a heads up that the DataProtectionCertificate.pfx file I got with my Boilerplate expired yesterday (Jan 21, 2025). It took me more hours than I'd like to admit to figure this out with SignalR continually blowing up saying the Context.User was not authenticated after login.
I created a new service for the API project that would handle checking the certificate and generating a new one if needed and then supplying it via the program.Services.cs file. I thought I'd share in case anyone finds it helpful for certificate and key rotation.
services.AddSingleton<CertificateService>();
using var scope = services.BuildServiceProvider().CreateScope();
var certificateService = scope.ServiceProvider.GetRequiredService<CertificateService>();
// Get both current and previous certificates
var (currentCert, previousCert) = certificateService.GetOrCreateCertificate("DataProtectionCertificate.pfx", appSettings.DataProtectionCertificatePassword);
var dataProtection = services.AddDataProtection()
.PersistKeysToDbContext<AppDbContext>()
.ProtectKeysWithCertificate(currentCert)
.SetDefaultKeyLifetime(TimeSpan.FromDays(30));
if (previousCert != null)
{
dataProtection.UnprotectKeysWithAnyCertificate(previousCert, currentCert);
Log.Information("Configured data protection with both current and previous certificates");
}
public class CertificateService
{
private readonly ILogger<CertificateService> _logger;
private readonly string _basePath;
public CertificateService(ILogger<CertificateService> logger)
{
_logger = logger;
_basePath = AppContext.BaseDirectory;
}
public (X509Certificate2 Current, X509Certificate2? Previous) GetOrCreateCertificate(string certificateName, string currentPassword)
{
var certificatePath = Path.Combine(_basePath, certificateName);
var backupPath = $"{certificatePath}.bak";
try
{
X509Certificate2? previousCert = null;
X509Certificate2? currentCert = null;
// Try to load existing certificate as current
if (File.Exists(certificatePath))
{
try
{
currentCert = new X509Certificate2(certificatePath, currentPassword, OperatingSystem.IsWindows() ? X509KeyStorageFlags.EphemeralKeySet : X509KeyStorageFlags.DefaultKeySet);
if (IsValidCertificate(currentCert))
{
// Even if current cert is valid, try to load previous for existing keys
if (File.Exists(backupPath))
{
try
{
previousCert = new X509Certificate2(backupPath, currentPassword, OperatingSystem.IsWindows() ? X509KeyStorageFlags.EphemeralKeySet : X509KeyStorageFlags.DefaultKeySet);
_logger.LogInformation("Loaded previous certificate for existing key decryption");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load previous certificate");
}
}
_logger.LogInformation("Using existing valid certificate with previous cert {hasPrevious}", previousCert != null ? "available" : "not available");
return (currentCert, previousCert);
}
// If current cert is expired, keep it as previous and generate new
previousCert = currentCert;
_logger.LogWarning("Current certificate expired. Will generate new one and keep old for existing keys");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load current certificate");
}
}
// Generate new certificate
currentCert = GenerateNewCertificate(certificatePath, currentPassword);
// If we didn't already set previous cert from expired current, try to load from backup
if (previousCert == null && File.Exists(backupPath))
{
try
{
previousCert = new X509Certificate2(backupPath, currentPassword, OperatingSystem.IsWindows() ? X509KeyStorageFlags.EphemeralKeySet : X509KeyStorageFlags.DefaultKeySet);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load previous certificate from backup");
}
}
return (currentCert, previousCert);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error managing certificates at {CertPath}", certificatePath);
throw;
}
}
private bool IsValidCertificate(X509Certificate2 cert)
{
var now = DateTimeOffset.UtcNow;
var daysUntilExpiry = (cert.NotAfter - now).TotalDays;
if (daysUntilExpiry <= 0)
{
_logger.LogError("Certificate expired on {ExpiryDate}", cert.NotAfter);
return false;
}
if (daysUntilExpiry <= 30)
{
_logger.LogWarning("Certificate will expire in {DaysRemaining} days on {ExpiryDate}",
(int)daysUntilExpiry, cert.NotAfter);
}
try
{
using var privateKey = cert.GetRSAPrivateKey();
if (privateKey == null)
{
_logger.LogError("Certificate private key is not accessible");
return false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error accessing certificate private key");
return false;
}
_logger.LogInformation("Certificate valid from {ValidFrom} to {ValidTo}",
cert.NotBefore, cert.NotAfter);
return true;
}
private X509Certificate2 GenerateNewCertificate(string path, string password)
{
using var rsa = RSA.Create(2048);
var distinguishedName = new X500DistinguishedName("CN=Bitplatform Data Protection");
var request = new CertificateRequest(
distinguishedName,
rsa,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(
new X509EnhancedKeyUsageExtension(
new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") },
false));
request.CertificateExtensions.Add(
new X509BasicConstraintsExtension(false, false, 0, true));
request.CertificateExtensions.Add(
new X509KeyUsageExtension(
X509KeyUsageFlags.DigitalSignature |
X509KeyUsageFlags.KeyEncipherment,
false));
var certificate = request.CreateSelfSigned(
DateTimeOffset.UtcNow.AddDays(-1),
DateTimeOffset.UtcNow.AddYears(2));
// Backup existing certificate
if (File.Exists(path))
{
var backupPath = $"{path}.bak";
File.Move(path, backupPath);
_logger.LogInformation("Backed up old certificate to {BackupPath}", backupPath);
}
File.WriteAllBytes(path, certificate.Export(X509ContentType.Pfx, password));
_logger.LogInformation("Generated new certificate at {CertPath}", path);
return new X509Certificate2(path, password, OperatingSystem.IsWindows() ? X509KeyStorageFlags.EphemeralKeySet : X509KeyStorageFlags.DefaultKeySet);
}
}
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
Type
Projects
Status
Done
Status
Closed