sln build fix (again), tests fixes, audit work and doctors work
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates cryptography provider licensing.
|
||||
/// </summary>
|
||||
public sealed class CryptoLicenseCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.crypto.license";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Crypto Licensing";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates licensed cryptography provider status and expiry";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["cryptography", "license", "compliance"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Only run if licensed providers are configured
|
||||
var cryptoProEnabled = context.Configuration.GetValue<bool?>("Cryptography:CryptoPro:Enabled");
|
||||
var licensedProviders = context.Configuration.GetSection("Cryptography:LicensedProviders").Get<string[]>();
|
||||
|
||||
return cryptoProEnabled == true || (licensedProviders?.Length > 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
|
||||
|
||||
var licensedProviders = context.Configuration.GetSection("Cryptography:LicensedProviders").Get<string[]>()
|
||||
?? [];
|
||||
|
||||
var issues = new List<string>();
|
||||
var licenseInfo = new Dictionary<string, string>();
|
||||
var checkedProviders = new List<string>();
|
||||
|
||||
// Check CryptoPro license
|
||||
var cryptoProEnabled = context.Configuration.GetValue<bool?>("Cryptography:CryptoPro:Enabled");
|
||||
if (cryptoProEnabled == true)
|
||||
{
|
||||
CheckCryptoProLicense(issues, licenseInfo, context.Configuration);
|
||||
checkedProviders.Add("CryptoPro");
|
||||
}
|
||||
|
||||
// Check other licensed providers
|
||||
foreach (var provider in licensedProviders)
|
||||
{
|
||||
if (checkedProviders.Contains(provider, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
CheckProviderLicense(provider, issues, licenseInfo, context.Configuration);
|
||||
checkedProviders.Add(provider);
|
||||
}
|
||||
|
||||
licenseInfo["CheckedProviders"] = string.Join(", ", checkedProviders);
|
||||
|
||||
if (checkedProviders.Count == 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Info("No licensed cryptography providers configured")
|
||||
.WithEvidence("License configuration", e =>
|
||||
{
|
||||
e.Add("LicensedProviders", "(none)");
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{issues.Count} crypto license issue(s)")
|
||||
.WithEvidence("License configuration", e =>
|
||||
{
|
||||
foreach (var kvp in licenseInfo)
|
||||
{
|
||||
e.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Verify license", "Check license file exists and is valid")
|
||||
.AddManualStep(2, "Renew license", "Contact vendor to renew expired licenses")
|
||||
.AddManualStep(3, "Configure license path", "Set Cryptography:<Provider>:LicensePath in configuration"))
|
||||
.WithVerification("stella doctor --check check.crypto.license")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass($"{checkedProviders.Count} crypto license(s) valid")
|
||||
.WithEvidence("License configuration", e =>
|
||||
{
|
||||
foreach (var kvp in licenseInfo)
|
||||
{
|
||||
e.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
|
||||
private static void CheckCryptoProLicense(List<string> issues, Dictionary<string, string> licenseInfo, IConfiguration config)
|
||||
{
|
||||
var licensePath = config.GetValue<string>("Cryptography:CryptoPro:LicensePath");
|
||||
var licenseKey = config.GetValue<string>("Cryptography:CryptoPro:LicenseKey");
|
||||
var expiryDate = config.GetValue<string>("Cryptography:CryptoPro:ExpiryDate");
|
||||
|
||||
licenseInfo["CryptoPro_LicenseConfigured"] = (!string.IsNullOrWhiteSpace(licensePath) || !string.IsNullOrWhiteSpace(licenseKey)).ToString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(licensePath))
|
||||
{
|
||||
if (!File.Exists(licensePath))
|
||||
{
|
||||
issues.Add($"CryptoPro license file not found: {licensePath}");
|
||||
}
|
||||
licenseInfo["CryptoPro_LicensePath"] = licensePath;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(expiryDate))
|
||||
{
|
||||
if (DateTimeOffset.TryParse(expiryDate, CultureInfo.InvariantCulture, DateTimeStyles.None, out var expiry))
|
||||
{
|
||||
licenseInfo["CryptoPro_Expiry"] = expiry.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
|
||||
var daysUntilExpiry = (expiry - DateTimeOffset.UtcNow).TotalDays;
|
||||
if (daysUntilExpiry < 0)
|
||||
{
|
||||
issues.Add("CryptoPro license has expired");
|
||||
}
|
||||
else if (daysUntilExpiry < 30)
|
||||
{
|
||||
issues.Add($"CryptoPro license expires in {daysUntilExpiry:F0} days");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckProviderLicense(string provider, List<string> issues, Dictionary<string, string> licenseInfo, IConfiguration config)
|
||||
{
|
||||
var section = $"Cryptography:{provider}";
|
||||
var licensePath = config.GetValue<string>($"{section}:LicensePath");
|
||||
var expiryDate = config.GetValue<string>($"{section}:ExpiryDate");
|
||||
|
||||
var prefix = provider.Replace(" ", "_");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(licensePath))
|
||||
{
|
||||
licenseInfo[$"{prefix}_LicensePath"] = licensePath;
|
||||
if (!File.Exists(licensePath))
|
||||
{
|
||||
issues.Add($"{provider} license file not found: {licensePath}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(expiryDate))
|
||||
{
|
||||
if (DateTimeOffset.TryParse(expiryDate, CultureInfo.InvariantCulture, DateTimeStyles.None, out var expiry))
|
||||
{
|
||||
licenseInfo[$"{prefix}_Expiry"] = expiry.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
|
||||
var daysUntilExpiry = (expiry - DateTimeOffset.UtcNow).TotalDays;
|
||||
if (daysUntilExpiry < 0)
|
||||
{
|
||||
issues.Add($"{provider} license has expired");
|
||||
}
|
||||
else if (daysUntilExpiry < 30)
|
||||
{
|
||||
issues.Add($"{provider} license expires in {daysUntilExpiry:F0} days");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates CryptoPro CSP (Windows GOST provider) availability.
|
||||
/// </summary>
|
||||
public sealed class CryptoProCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.crypto.cryptopro";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "CryptoPro CSP";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates CryptoPro CSP installation and configuration (Windows)";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["cryptography", "cryptopro", "gost", "windows", "regional"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(200);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Only run if CryptoPro is enabled or on Windows with GOST enabled
|
||||
var cryptoProEnabled = context.Configuration.GetValue<bool?>("Cryptography:CryptoPro:Enabled");
|
||||
|
||||
if (cryptoProEnabled == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if running on Windows with GOST enabled
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var gostEnabled = context.Configuration.GetValue<bool?>("Cryptography:Gost:Enabled");
|
||||
var gostProvider = context.Configuration.GetValue<string>("Cryptography:Gost:Provider");
|
||||
|
||||
return gostEnabled == true && gostProvider?.Equals("cryptopro", StringComparison.OrdinalIgnoreCase) == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
|
||||
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Skip("CryptoPro CSP is only available on Windows")
|
||||
.WithEvidence("CryptoPro", e =>
|
||||
{
|
||||
e.Add("Platform", RuntimeInformation.OSDescription);
|
||||
e.Add("Recommendation", "Use OpenSSL GOST engine on Linux");
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
|
||||
var issues = new List<string>();
|
||||
var cryptoProInfo = new Dictionary<string, string>();
|
||||
|
||||
// Check build flag
|
||||
var buildFlag = Environment.GetEnvironmentVariable("STELLAOPS_CRYPTO_PRO");
|
||||
cryptoProInfo["STELLAOPS_CRYPTO_PRO"] = buildFlag ?? "(not set)";
|
||||
|
||||
if (buildFlag != "1" && buildFlag?.ToLowerInvariant() != "true")
|
||||
{
|
||||
issues.Add("STELLAOPS_CRYPTO_PRO build flag not set - CryptoPro support may not be compiled in");
|
||||
}
|
||||
|
||||
// Check installation paths
|
||||
var installPaths = new[]
|
||||
{
|
||||
@"C:\Program Files\Crypto Pro\CSP",
|
||||
@"C:\Program Files (x86)\Crypto Pro\CSP"
|
||||
};
|
||||
|
||||
var foundPath = installPaths.FirstOrDefault(Directory.Exists);
|
||||
if (foundPath != null)
|
||||
{
|
||||
cryptoProInfo["InstallPath"] = foundPath;
|
||||
|
||||
// Check for key executables
|
||||
var csptest = Path.Combine(foundPath, "csptest.exe");
|
||||
var cryptcp = Path.Combine(foundPath, "cryptcp.exe");
|
||||
|
||||
cryptoProInfo["CspTestExists"] = File.Exists(csptest).ToString();
|
||||
cryptoProInfo["CryptcpExists"] = File.Exists(cryptcp).ToString();
|
||||
|
||||
// Try to detect version from files
|
||||
var versionFile = Path.Combine(foundPath, "version.txt");
|
||||
if (File.Exists(versionFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
var version = File.ReadAllText(versionFile).Trim();
|
||||
cryptoProInfo["Version"] = version;
|
||||
}
|
||||
catch
|
||||
{
|
||||
cryptoProInfo["Version"] = "(cannot read)";
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
issues.Add("CryptoPro CSP installation not found");
|
||||
cryptoProInfo["InstallPath"] = "(not found)";
|
||||
}
|
||||
|
||||
// Check for license
|
||||
var licensePath = context.Configuration.GetValue<string>("Cryptography:CryptoPro:LicensePath");
|
||||
if (!string.IsNullOrWhiteSpace(licensePath))
|
||||
{
|
||||
cryptoProInfo["LicensePath"] = licensePath;
|
||||
if (!File.Exists(licensePath))
|
||||
{
|
||||
issues.Add($"CryptoPro license file not found: {licensePath}");
|
||||
}
|
||||
}
|
||||
|
||||
// Check for configured containers
|
||||
var containerName = context.Configuration.GetValue<string>("Cryptography:CryptoPro:ContainerName");
|
||||
if (!string.IsNullOrWhiteSpace(containerName))
|
||||
{
|
||||
cryptoProInfo["ContainerName"] = containerName;
|
||||
}
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{issues.Count} CryptoPro issue(s)")
|
||||
.WithEvidence("CryptoPro configuration", e =>
|
||||
{
|
||||
foreach (var kvp in cryptoProInfo)
|
||||
{
|
||||
e.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Install CryptoPro", "Download and install CryptoPro CSP from cryptopro.ru")
|
||||
.AddManualStep(2, "Set build flag", "Set STELLAOPS_CRYPTO_PRO=1 environment variable")
|
||||
.AddManualStep(3, "Configure license", "Configure Cryptography:CryptoPro:LicensePath"))
|
||||
.WithVerification("stella doctor --check check.crypto.cryptopro")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass("CryptoPro CSP is installed and configured")
|
||||
.WithEvidence("CryptoPro configuration", e =>
|
||||
{
|
||||
foreach (var kvp in cryptoProInfo)
|
||||
{
|
||||
e.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates cryptography provider availability.
|
||||
/// </summary>
|
||||
public sealed class CryptoProviderAvailabilityCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.crypto.provider";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Crypto Provider Availability";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates cryptographic providers are available and properly configured";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["cryptography", "provider", "security"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
|
||||
|
||||
var configuredProviders = context.Configuration.GetSection("Cryptography:Providers").Get<string[]>()
|
||||
?? ["default"];
|
||||
|
||||
var defaultAlgorithm = context.Configuration.GetValue<string>("Cryptography:DefaultAlgorithm")
|
||||
?? "ecdsa-p256";
|
||||
|
||||
var issues = new List<string>();
|
||||
var availableProviders = new List<string>();
|
||||
|
||||
// Check default .NET crypto providers
|
||||
try
|
||||
{
|
||||
using var ecdsa = System.Security.Cryptography.ECDsa.Create();
|
||||
if (ecdsa != null)
|
||||
{
|
||||
availableProviders.Add("ECDSA (P-256/P-384/P-521)");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
issues.Add("ECDSA provider not available");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var rsa = System.Security.Cryptography.RSA.Create();
|
||||
if (rsa != null)
|
||||
{
|
||||
availableProviders.Add("RSA (2048/4096)");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
issues.Add("RSA provider not available");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var aes = System.Security.Cryptography.Aes.Create();
|
||||
if (aes != null)
|
||||
{
|
||||
availableProviders.Add("AES (128/256)");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
issues.Add("AES provider not available");
|
||||
}
|
||||
|
||||
// Check EdDSA if configured
|
||||
if (configuredProviders.Contains("ed25519", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ed25519 is available in .NET 9+
|
||||
availableProviders.Add("Ed25519");
|
||||
}
|
||||
catch
|
||||
{
|
||||
issues.Add("Ed25519 provider not available");
|
||||
}
|
||||
}
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{issues.Count} crypto provider issue(s)")
|
||||
.WithEvidence("Crypto providers", e =>
|
||||
{
|
||||
e.Add("ConfiguredProviders", string.Join(", ", configuredProviders));
|
||||
e.Add("AvailableProviders", string.Join(", ", availableProviders));
|
||||
e.Add("DefaultAlgorithm", defaultAlgorithm);
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Check runtime", "Ensure .NET runtime supports required algorithms")
|
||||
.AddManualStep(2, "Install providers", "Install additional crypto libraries if needed"))
|
||||
.WithVerification("stella doctor --check check.crypto.provider")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass($"{availableProviders.Count} crypto provider(s) available")
|
||||
.WithEvidence("Crypto providers", e =>
|
||||
{
|
||||
e.Add("ConfiguredProviders", string.Join(", ", configuredProviders));
|
||||
e.Add("AvailableProviders", string.Join(", ", availableProviders));
|
||||
e.Add("DefaultAlgorithm", defaultAlgorithm);
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates eIDAS (EU qualified signatures) provider configuration.
|
||||
/// </summary>
|
||||
public sealed class EidasProviderCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.crypto.eidas";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "eIDAS Provider";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates eIDAS qualified signature provider configuration";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["cryptography", "eidas", "qualified", "eu", "regional"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Only run if eIDAS is configured or enabled
|
||||
var eidasEnabled = context.Configuration.GetValue<bool?>("Cryptography:Eidas:Enabled")
|
||||
?? context.Configuration.GetValue<bool?>("Cryptography:EnableEidas");
|
||||
|
||||
return eidasEnabled == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
|
||||
|
||||
var eidasProvider = context.Configuration.GetValue<string>("Cryptography:Eidas:Provider")
|
||||
?? "pkcs11";
|
||||
|
||||
var trustListUrl = context.Configuration.GetValue<string>("Cryptography:Eidas:TrustListUrl")
|
||||
?? "https://ec.europa.eu/tools/lotl/eu-lotl.xml";
|
||||
|
||||
var issues = new List<string>();
|
||||
var providerInfo = new Dictionary<string, string>
|
||||
{
|
||||
["ConfiguredProvider"] = eidasProvider,
|
||||
["TrustListUrl"] = trustListUrl
|
||||
};
|
||||
|
||||
switch (eidasProvider.ToLowerInvariant())
|
||||
{
|
||||
case "pkcs11":
|
||||
CheckPkcs11Eidas(issues, providerInfo, context.Configuration);
|
||||
break;
|
||||
|
||||
case "certificate":
|
||||
CheckCertificateEidas(issues, providerInfo, context.Configuration);
|
||||
break;
|
||||
|
||||
case "remote":
|
||||
CheckRemoteEidas(issues, providerInfo, context.Configuration);
|
||||
break;
|
||||
|
||||
default:
|
||||
issues.Add($"Unknown eIDAS provider: {eidasProvider}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{issues.Count} eIDAS provider issue(s)")
|
||||
.WithEvidence("eIDAS configuration", e =>
|
||||
{
|
||||
foreach (var kvp in providerInfo)
|
||||
{
|
||||
e.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Configure provider", "Configure PKCS#11 library or certificate store for eIDAS")
|
||||
.AddManualStep(2, "Verify trust list", "Ensure EU Trust List is accessible"))
|
||||
.WithVerification("stella doctor --check check.crypto.eidas")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass("eIDAS provider is configured")
|
||||
.WithEvidence("eIDAS configuration", e =>
|
||||
{
|
||||
foreach (var kvp in providerInfo)
|
||||
{
|
||||
e.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
|
||||
private static void CheckPkcs11Eidas(List<string> issues, Dictionary<string, string> providerInfo, IConfiguration config)
|
||||
{
|
||||
providerInfo["Provider"] = "PKCS#11 (Smart Card/HSM)";
|
||||
|
||||
var pkcs11Library = config.GetValue<string>("Cryptography:Eidas:Pkcs11Library");
|
||||
if (string.IsNullOrWhiteSpace(pkcs11Library))
|
||||
{
|
||||
issues.Add("PKCS#11 library path not configured for eIDAS");
|
||||
providerInfo["Library"] = "(not set)";
|
||||
}
|
||||
else if (!File.Exists(pkcs11Library))
|
||||
{
|
||||
issues.Add($"PKCS#11 library not found: {pkcs11Library}");
|
||||
providerInfo["Library"] = pkcs11Library;
|
||||
}
|
||||
else
|
||||
{
|
||||
providerInfo["Library"] = pkcs11Library;
|
||||
}
|
||||
|
||||
var slotId = config.GetValue<int?>("Cryptography:Eidas:SlotId");
|
||||
providerInfo["SlotId"] = slotId?.ToString() ?? "(auto-detect)";
|
||||
}
|
||||
|
||||
private static void CheckCertificateEidas(List<string> issues, Dictionary<string, string> providerInfo, IConfiguration config)
|
||||
{
|
||||
providerInfo["Provider"] = "Certificate Store";
|
||||
|
||||
var certThumbprint = config.GetValue<string>("Cryptography:Eidas:CertificateThumbprint");
|
||||
var certPath = config.GetValue<string>("Cryptography:Eidas:CertificatePath");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(certThumbprint))
|
||||
{
|
||||
providerInfo["CertificateThumbprint"] = certThumbprint;
|
||||
|
||||
// Try to find certificate in store
|
||||
try
|
||||
{
|
||||
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
|
||||
store.Open(OpenFlags.ReadOnly);
|
||||
var certs = store.Certificates.Find(
|
||||
X509FindType.FindByThumbprint,
|
||||
certThumbprint,
|
||||
validOnly: false);
|
||||
|
||||
if (certs.Count == 0)
|
||||
{
|
||||
issues.Add($"Certificate with thumbprint {certThumbprint[..8]}... not found in store");
|
||||
}
|
||||
else
|
||||
{
|
||||
var cert = certs[0];
|
||||
providerInfo["CertificateSubject"] = cert.Subject;
|
||||
providerInfo["CertificateExpiry"] = cert.NotAfter.ToString("yyyy-MM-dd");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
issues.Add($"Cannot access certificate store: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(certPath))
|
||||
{
|
||||
providerInfo["CertificatePath"] = certPath;
|
||||
if (!File.Exists(certPath))
|
||||
{
|
||||
issues.Add($"Certificate file not found: {certPath}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
issues.Add("No certificate thumbprint or path configured for eIDAS");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckRemoteEidas(List<string> issues, Dictionary<string, string> providerInfo, IConfiguration config)
|
||||
{
|
||||
providerInfo["Provider"] = "Remote Signing Service";
|
||||
|
||||
var remoteEndpoint = config.GetValue<string>("Cryptography:Eidas:RemoteEndpoint");
|
||||
if (string.IsNullOrWhiteSpace(remoteEndpoint))
|
||||
{
|
||||
issues.Add("Remote eIDAS signing endpoint not configured");
|
||||
providerInfo["Endpoint"] = "(not set)";
|
||||
}
|
||||
else
|
||||
{
|
||||
providerInfo["Endpoint"] = remoteEndpoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates FIPS 140-2/140-3 compliance mode.
|
||||
/// </summary>
|
||||
public sealed class FipsComplianceCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.crypto.fips";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "FIPS Compliance";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates FIPS 140-2/140-3 compliance mode is configured correctly";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["cryptography", "fips", "compliance", "security"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(50);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
|
||||
|
||||
var fipsRequired = context.Configuration.GetValue<bool?>("Cryptography:RequireFips")
|
||||
?? context.Configuration.GetValue<bool?>("Security:FipsMode");
|
||||
|
||||
var fipsEnabled = IsFipsModeEnabled();
|
||||
|
||||
if (fipsRequired == true && !fipsEnabled)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Fail("FIPS mode required but not enabled")
|
||||
.WithEvidence("FIPS configuration", e =>
|
||||
{
|
||||
e.Add("FipsRequired", "true");
|
||||
e.Add("FipsEnabled", "false");
|
||||
e.Add("Platform", RuntimeInformation.OSDescription);
|
||||
})
|
||||
.WithCauses("System FIPS mode is not enabled but configuration requires it")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Enable FIPS on Windows", "Set FIPS security policy in Windows Group Policy")
|
||||
.AddManualStep(2, "Enable FIPS on Linux", "Configure system crypto policy with fips-mode-setup"))
|
||||
.WithVerification("stella doctor --check check.crypto.fips")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (fipsRequired == false && fipsEnabled)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Info("FIPS mode enabled but not required by configuration")
|
||||
.WithEvidence("FIPS configuration", e =>
|
||||
{
|
||||
e.Add("FipsRequired", "false");
|
||||
e.Add("FipsEnabled", "true");
|
||||
e.Add("Platform", RuntimeInformation.OSDescription);
|
||||
e.Add("Note", "Running in FIPS mode may restrict some algorithms");
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (fipsRequired == null)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Info($"FIPS mode: {(fipsEnabled ? "enabled" : "disabled")} (not explicitly configured)")
|
||||
.WithEvidence("FIPS configuration", e =>
|
||||
{
|
||||
e.Add("FipsRequired", "(not set)");
|
||||
e.Add("FipsEnabled", fipsEnabled.ToString());
|
||||
e.Add("Platform", RuntimeInformation.OSDescription);
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass("FIPS compliance matches requirements")
|
||||
.WithEvidence("FIPS configuration", e =>
|
||||
{
|
||||
e.Add("FipsRequired", fipsRequired.ToString()!);
|
||||
e.Add("FipsEnabled", fipsEnabled.ToString());
|
||||
e.Add("Platform", RuntimeInformation.OSDescription);
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
|
||||
private static bool IsFipsModeEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check environment variable first
|
||||
var envFips = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_SECURITY_CRYPTOGRAPHY_USELEGACYMACOPENSSL");
|
||||
if (envFips == "0")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// On Windows, check registry (simplified check)
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Try to detect FIPS via CryptoConfig
|
||||
try
|
||||
{
|
||||
// In FIPS mode, certain algorithms will be unavailable
|
||||
using var md5 = System.Security.Cryptography.MD5.Create();
|
||||
// If MD5 works, FIPS is likely not enforced
|
||||
return false;
|
||||
}
|
||||
catch (System.Security.Cryptography.CryptographicException)
|
||||
{
|
||||
// MD5 blocked - FIPS mode likely enabled
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// On Linux, check crypto policies
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
var policyFile = "/etc/crypto-policies/state/current";
|
||||
if (File.Exists(policyFile))
|
||||
{
|
||||
var policy = File.ReadAllText(policyFile).Trim();
|
||||
return policy.Contains("FIPS", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates GOST (Russian) cryptography provider availability.
|
||||
/// </summary>
|
||||
public sealed class GostProviderCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.crypto.gost";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "GOST Cryptography";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates GOST R 34.10-2012 / R 34.11-2012 cryptography provider";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["cryptography", "gost", "regional", "russia"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Only run if GOST is configured or enabled
|
||||
var gostEnabled = context.Configuration.GetValue<bool?>("Cryptography:Gost:Enabled")
|
||||
?? context.Configuration.GetValue<bool?>("Cryptography:EnableGost");
|
||||
|
||||
return gostEnabled == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
|
||||
|
||||
var gostProvider = context.Configuration.GetValue<string>("Cryptography:Gost:Provider")
|
||||
?? "openssl-gost";
|
||||
|
||||
var gostEndpoint = context.Configuration.GetValue<string>("Cryptography:Gost:Endpoint");
|
||||
|
||||
var issues = new List<string>();
|
||||
var providerInfo = new Dictionary<string, string>();
|
||||
|
||||
// Check which GOST provider is configured
|
||||
switch (gostProvider.ToLowerInvariant())
|
||||
{
|
||||
case "openssl-gost":
|
||||
CheckOpenSslGost(issues, providerInfo);
|
||||
break;
|
||||
|
||||
case "cryptopro":
|
||||
CheckCryptoProGost(issues, providerInfo);
|
||||
break;
|
||||
|
||||
case "pkcs11-gost":
|
||||
CheckPkcs11Gost(issues, providerInfo, context.Configuration);
|
||||
break;
|
||||
|
||||
case "remote":
|
||||
if (string.IsNullOrWhiteSpace(gostEndpoint))
|
||||
{
|
||||
issues.Add("Remote GOST provider configured but no endpoint specified");
|
||||
}
|
||||
providerInfo["Provider"] = "Remote Service";
|
||||
providerInfo["Endpoint"] = gostEndpoint ?? "(not set)";
|
||||
break;
|
||||
|
||||
default:
|
||||
issues.Add($"Unknown GOST provider: {gostProvider}");
|
||||
break;
|
||||
}
|
||||
|
||||
providerInfo["ConfiguredProvider"] = gostProvider;
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{issues.Count} GOST provider issue(s)")
|
||||
.WithEvidence("GOST cryptography", e =>
|
||||
{
|
||||
foreach (var kvp in providerInfo)
|
||||
{
|
||||
e.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Install provider", "Install the configured GOST provider (OpenSSL GOST engine, CryptoPro CSP, or PKCS#11)")
|
||||
.AddManualStep(2, "Configure endpoint", "For remote providers, configure the service endpoint"))
|
||||
.WithVerification("stella doctor --check check.crypto.gost")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass("GOST cryptography provider is configured")
|
||||
.WithEvidence("GOST cryptography", e =>
|
||||
{
|
||||
foreach (var kvp in providerInfo)
|
||||
{
|
||||
e.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
|
||||
private static void CheckOpenSslGost(List<string> issues, Dictionary<string, string> providerInfo)
|
||||
{
|
||||
providerInfo["Provider"] = "OpenSSL GOST Engine";
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
// Check for GOST engine in common locations
|
||||
var enginePaths = new[]
|
||||
{
|
||||
"/usr/lib/x86_64-linux-gnu/engines-3/gost.so",
|
||||
"/usr/lib64/engines-3/gost.so",
|
||||
"/usr/lib/engines/gost.so"
|
||||
};
|
||||
|
||||
var foundEngine = enginePaths.FirstOrDefault(File.Exists);
|
||||
if (foundEngine != null)
|
||||
{
|
||||
providerInfo["EnginePath"] = foundEngine;
|
||||
}
|
||||
else
|
||||
{
|
||||
issues.Add("OpenSSL GOST engine not found in standard locations");
|
||||
providerInfo["EnginePath"] = "(not found)";
|
||||
}
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
issues.Add("OpenSSL GOST engine is primarily supported on Linux");
|
||||
providerInfo["Note"] = "Consider using CryptoPro CSP on Windows";
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckCryptoProGost(List<string> issues, Dictionary<string, string> providerInfo)
|
||||
{
|
||||
providerInfo["Provider"] = "CryptoPro CSP";
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Check for CryptoPro installation
|
||||
var cryptoProPaths = new[]
|
||||
{
|
||||
@"C:\Program Files\Crypto Pro\CSP",
|
||||
@"C:\Program Files (x86)\Crypto Pro\CSP"
|
||||
};
|
||||
|
||||
var foundPath = cryptoProPaths.FirstOrDefault(Directory.Exists);
|
||||
if (foundPath != null)
|
||||
{
|
||||
providerInfo["InstallPath"] = foundPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
issues.Add("CryptoPro CSP not found");
|
||||
providerInfo["InstallPath"] = "(not found)";
|
||||
}
|
||||
|
||||
// Check build flag
|
||||
var buildFlag = Environment.GetEnvironmentVariable("STELLAOPS_CRYPTO_PRO");
|
||||
providerInfo["BuildFlag"] = buildFlag ?? "(not set)";
|
||||
if (buildFlag != "1" && buildFlag?.ToLowerInvariant() != "true")
|
||||
{
|
||||
issues.Add("STELLAOPS_CRYPTO_PRO build flag not set");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
issues.Add("CryptoPro CSP is only supported on Windows");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckPkcs11Gost(List<string> issues, Dictionary<string, string> providerInfo, Microsoft.Extensions.Configuration.IConfiguration config)
|
||||
{
|
||||
providerInfo["Provider"] = "PKCS#11 GOST Token";
|
||||
|
||||
var pkcs11Library = config.GetValue<string>("Cryptography:Gost:Pkcs11Library");
|
||||
if (string.IsNullOrWhiteSpace(pkcs11Library))
|
||||
{
|
||||
issues.Add("PKCS#11 library path not configured");
|
||||
providerInfo["Library"] = "(not set)";
|
||||
}
|
||||
else if (!File.Exists(pkcs11Library))
|
||||
{
|
||||
issues.Add($"PKCS#11 library not found: {pkcs11Library}");
|
||||
providerInfo["Library"] = pkcs11Library;
|
||||
}
|
||||
else
|
||||
{
|
||||
providerInfo["Library"] = pkcs11Library;
|
||||
}
|
||||
|
||||
var slotId = config.GetValue<int?>("Cryptography:Gost:SlotId");
|
||||
providerInfo["SlotId"] = slotId?.ToString() ?? "0";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates HSM (Hardware Security Module) connectivity.
|
||||
/// </summary>
|
||||
public sealed class HsmConnectivityCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.crypto.hsm";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "HSM Connectivity";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates HSM/PKCS#11 hardware token connectivity";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["cryptography", "hsm", "pkcs11", "hardware", "security"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Only run if HSM is configured
|
||||
var hsmEnabled = context.Configuration.GetValue<bool?>("Cryptography:Hsm:Enabled")
|
||||
?? context.Configuration.GetValue<bool?>("Cryptography:EnableHsm");
|
||||
|
||||
return hsmEnabled == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
|
||||
|
||||
var hsmType = context.Configuration.GetValue<string>("Cryptography:Hsm:Type")
|
||||
?? "pkcs11";
|
||||
|
||||
var pkcs11Library = context.Configuration.GetValue<string>("Cryptography:Hsm:Pkcs11Library")
|
||||
?? context.Configuration.GetValue<string>("Cryptography:Pkcs11:Library");
|
||||
|
||||
var slotId = context.Configuration.GetValue<int?>("Cryptography:Hsm:SlotId")
|
||||
?? context.Configuration.GetValue<int?>("Cryptography:Pkcs11:SlotId")
|
||||
?? 0;
|
||||
|
||||
var issues = new List<string>();
|
||||
var hsmInfo = new Dictionary<string, string>
|
||||
{
|
||||
["HsmType"] = hsmType,
|
||||
["SlotId"] = slotId.ToString()
|
||||
};
|
||||
|
||||
switch (hsmType.ToLowerInvariant())
|
||||
{
|
||||
case "pkcs11":
|
||||
CheckPkcs11Hsm(issues, hsmInfo, pkcs11Library);
|
||||
break;
|
||||
|
||||
case "softhsm":
|
||||
CheckSoftHsm(issues, hsmInfo);
|
||||
break;
|
||||
|
||||
case "azure":
|
||||
case "azurekeyvault":
|
||||
CheckAzureKeyVault(issues, hsmInfo, context.Configuration);
|
||||
break;
|
||||
|
||||
case "aws":
|
||||
case "awskms":
|
||||
CheckAwsKms(issues, hsmInfo, context.Configuration);
|
||||
break;
|
||||
|
||||
case "gcp":
|
||||
case "gcpkms":
|
||||
CheckGcpKms(issues, hsmInfo, context.Configuration);
|
||||
break;
|
||||
|
||||
case "hashicorp":
|
||||
case "vault":
|
||||
CheckHashiCorpVault(issues, hsmInfo, context.Configuration);
|
||||
break;
|
||||
|
||||
default:
|
||||
issues.Add($"Unknown HSM type: {hsmType}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{issues.Count} HSM connectivity issue(s)")
|
||||
.WithEvidence("HSM configuration", e =>
|
||||
{
|
||||
foreach (var kvp in hsmInfo)
|
||||
{
|
||||
e.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Install PKCS#11 library", "Ensure the HSM PKCS#11 library is installed")
|
||||
.AddManualStep(2, "Check connectivity", "Verify network/USB connectivity to HSM")
|
||||
.AddManualStep(3, "Verify credentials", "Ensure HSM credentials are configured"))
|
||||
.WithVerification("stella doctor --check check.crypto.hsm")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass("HSM is accessible")
|
||||
.WithEvidence("HSM configuration", e =>
|
||||
{
|
||||
foreach (var kvp in hsmInfo)
|
||||
{
|
||||
e.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
|
||||
private static void CheckPkcs11Hsm(List<string> issues, Dictionary<string, string> hsmInfo, string? pkcs11Library)
|
||||
{
|
||||
hsmInfo["Provider"] = "PKCS#11";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pkcs11Library))
|
||||
{
|
||||
issues.Add("PKCS#11 library path not configured");
|
||||
hsmInfo["Library"] = "(not set)";
|
||||
return;
|
||||
}
|
||||
|
||||
hsmInfo["Library"] = pkcs11Library;
|
||||
|
||||
if (!File.Exists(pkcs11Library))
|
||||
{
|
||||
issues.Add($"PKCS#11 library not found: {pkcs11Library}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Library exists - basic check passed
|
||||
hsmInfo["LibraryExists"] = "true";
|
||||
}
|
||||
|
||||
private static void CheckSoftHsm(List<string> issues, Dictionary<string, string> hsmInfo)
|
||||
{
|
||||
hsmInfo["Provider"] = "SoftHSM";
|
||||
|
||||
// Check common SoftHSM library locations
|
||||
var libraryPaths = new[]
|
||||
{
|
||||
"/usr/lib/softhsm/libsofthsm2.so",
|
||||
"/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so",
|
||||
"/usr/local/lib/softhsm/libsofthsm2.so"
|
||||
};
|
||||
|
||||
var foundLibrary = libraryPaths.FirstOrDefault(File.Exists);
|
||||
if (foundLibrary != null)
|
||||
{
|
||||
hsmInfo["Library"] = foundLibrary;
|
||||
}
|
||||
else
|
||||
{
|
||||
issues.Add("SoftHSM library not found in standard locations");
|
||||
hsmInfo["Library"] = "(not found)";
|
||||
}
|
||||
|
||||
// Check for token directory
|
||||
var tokenDir = Environment.GetEnvironmentVariable("SOFTHSM2_CONF");
|
||||
hsmInfo["ConfigPath"] = tokenDir ?? "(using default)";
|
||||
}
|
||||
|
||||
private static void CheckAzureKeyVault(List<string> issues, Dictionary<string, string> hsmInfo, IConfiguration config)
|
||||
{
|
||||
hsmInfo["Provider"] = "Azure Key Vault";
|
||||
|
||||
var vaultUrl = config.GetValue<string>("Cryptography:Hsm:Azure:VaultUrl")
|
||||
?? config.GetValue<string>("AzureKeyVault:VaultUrl");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(vaultUrl))
|
||||
{
|
||||
issues.Add("Azure Key Vault URL not configured");
|
||||
hsmInfo["VaultUrl"] = "(not set)";
|
||||
}
|
||||
else
|
||||
{
|
||||
hsmInfo["VaultUrl"] = vaultUrl;
|
||||
}
|
||||
|
||||
// Check for credential configuration
|
||||
var tenantId = config.GetValue<string>("Cryptography:Hsm:Azure:TenantId")
|
||||
?? config.GetValue<string>("AzureKeyVault:TenantId");
|
||||
hsmInfo["TenantConfigured"] = (!string.IsNullOrWhiteSpace(tenantId)).ToString();
|
||||
}
|
||||
|
||||
private static void CheckAwsKms(List<string> issues, Dictionary<string, string> hsmInfo, IConfiguration config)
|
||||
{
|
||||
hsmInfo["Provider"] = "AWS KMS";
|
||||
|
||||
var region = config.GetValue<string>("Cryptography:Hsm:Aws:Region")
|
||||
?? config.GetValue<string>("AWS:Region");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(region))
|
||||
{
|
||||
issues.Add("AWS region not configured");
|
||||
hsmInfo["Region"] = "(not set)";
|
||||
}
|
||||
else
|
||||
{
|
||||
hsmInfo["Region"] = region;
|
||||
}
|
||||
|
||||
var keyId = config.GetValue<string>("Cryptography:Hsm:Aws:KeyId");
|
||||
hsmInfo["KeyConfigured"] = (!string.IsNullOrWhiteSpace(keyId)).ToString();
|
||||
}
|
||||
|
||||
private static void CheckGcpKms(List<string> issues, Dictionary<string, string> hsmInfo, IConfiguration config)
|
||||
{
|
||||
hsmInfo["Provider"] = "Google Cloud KMS";
|
||||
|
||||
var projectId = config.GetValue<string>("Cryptography:Hsm:Gcp:ProjectId")
|
||||
?? config.GetValue<string>("GCP:ProjectId");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(projectId))
|
||||
{
|
||||
issues.Add("GCP project ID not configured");
|
||||
hsmInfo["ProjectId"] = "(not set)";
|
||||
}
|
||||
else
|
||||
{
|
||||
hsmInfo["ProjectId"] = projectId;
|
||||
}
|
||||
|
||||
var keyRing = config.GetValue<string>("Cryptography:Hsm:Gcp:KeyRing");
|
||||
hsmInfo["KeyRingConfigured"] = (!string.IsNullOrWhiteSpace(keyRing)).ToString();
|
||||
}
|
||||
|
||||
private static void CheckHashiCorpVault(List<string> issues, Dictionary<string, string> hsmInfo, IConfiguration config)
|
||||
{
|
||||
hsmInfo["Provider"] = "HashiCorp Vault";
|
||||
|
||||
var vaultAddr = config.GetValue<string>("Cryptography:Hsm:Vault:Address")
|
||||
?? Environment.GetEnvironmentVariable("VAULT_ADDR");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(vaultAddr))
|
||||
{
|
||||
issues.Add("HashiCorp Vault address not configured");
|
||||
hsmInfo["Address"] = "(not set)";
|
||||
}
|
||||
else
|
||||
{
|
||||
hsmInfo["Address"] = vaultAddr;
|
||||
}
|
||||
|
||||
var transitPath = config.GetValue<string>("Cryptography:Hsm:Vault:TransitPath") ?? "transit";
|
||||
hsmInfo["TransitPath"] = transitPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates SM2/SM3/SM4 (Chinese) cryptography provider availability.
|
||||
/// </summary>
|
||||
public sealed class SmProviderCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.crypto.sm";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "SM Cryptography";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates SM2/SM3/SM4 (GB/T) cryptography provider";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["cryptography", "sm2", "sm3", "sm4", "regional", "china"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Only run if SM crypto is configured or enabled
|
||||
var smEnabled = context.Configuration.GetValue<bool?>("Cryptography:Sm:Enabled")
|
||||
?? context.Configuration.GetValue<bool?>("Cryptography:EnableSm");
|
||||
|
||||
return smEnabled == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
|
||||
|
||||
var smProvider = context.Configuration.GetValue<string>("Cryptography:Sm:Provider")
|
||||
?? "smsoft";
|
||||
|
||||
var smEndpoint = context.Configuration.GetValue<string>("Cryptography:Sm:Endpoint")
|
||||
?? context.Configuration.GetValue<string>("SmRemote:Endpoint");
|
||||
|
||||
var issues = new List<string>();
|
||||
var providerInfo = new Dictionary<string, string>();
|
||||
|
||||
// Check environment gate for SM providers
|
||||
var smSoftAllowed = Environment.GetEnvironmentVariable("SM_SOFT_ALLOWED");
|
||||
providerInfo["SM_SOFT_ALLOWED"] = smSoftAllowed ?? "(not set)";
|
||||
|
||||
switch (smProvider.ToLowerInvariant())
|
||||
{
|
||||
case "smsoft":
|
||||
CheckSmSoft(issues, providerInfo);
|
||||
break;
|
||||
|
||||
case "smremote":
|
||||
case "remote":
|
||||
CheckSmRemote(issues, providerInfo, smEndpoint);
|
||||
break;
|
||||
|
||||
case "bouncycastle":
|
||||
CheckBouncyCastleSm(issues, providerInfo);
|
||||
break;
|
||||
|
||||
default:
|
||||
issues.Add($"Unknown SM provider: {smProvider}");
|
||||
break;
|
||||
}
|
||||
|
||||
providerInfo["ConfiguredProvider"] = smProvider;
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{issues.Count} SM provider issue(s)")
|
||||
.WithEvidence("SM cryptography", e =>
|
||||
{
|
||||
foreach (var kvp in providerInfo)
|
||||
{
|
||||
e.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Set environment gate", "Set SM_SOFT_ALLOWED=1 to enable SM software providers")
|
||||
.AddManualStep(2, "Configure SmRemote", "Configure SmRemote:Endpoint for remote SM crypto service"))
|
||||
.WithVerification("stella doctor --check check.crypto.sm")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass("SM cryptography provider is configured")
|
||||
.WithEvidence("SM cryptography", e =>
|
||||
{
|
||||
foreach (var kvp in providerInfo)
|
||||
{
|
||||
e.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
|
||||
private static void CheckSmSoft(List<string> issues, Dictionary<string, string> providerInfo)
|
||||
{
|
||||
providerInfo["Provider"] = "SmSoft (Software Implementation)";
|
||||
|
||||
var smSoftAllowed = Environment.GetEnvironmentVariable("SM_SOFT_ALLOWED");
|
||||
if (smSoftAllowed != "1" && smSoftAllowed?.ToLowerInvariant() != "true")
|
||||
{
|
||||
issues.Add("SM_SOFT_ALLOWED environment variable not set - SmSoft provider may not be available");
|
||||
}
|
||||
|
||||
// Check if BouncyCastle is available for SM algorithms
|
||||
providerInfo["Implementation"] = "BouncyCastle (managed)";
|
||||
}
|
||||
|
||||
private static void CheckSmRemote(List<string> issues, Dictionary<string, string> providerInfo, string? endpoint)
|
||||
{
|
||||
providerInfo["Provider"] = "SmRemote (Remote Service)";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
issues.Add("SmRemote endpoint not configured");
|
||||
providerInfo["Endpoint"] = "(not set)";
|
||||
}
|
||||
else
|
||||
{
|
||||
providerInfo["Endpoint"] = endpoint;
|
||||
|
||||
// Check if endpoint looks valid
|
||||
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var uri))
|
||||
{
|
||||
issues.Add($"SmRemote endpoint is not a valid URI: {endpoint}");
|
||||
}
|
||||
else
|
||||
{
|
||||
providerInfo["Host"] = uri.Host;
|
||||
providerInfo["Port"] = uri.Port.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckBouncyCastleSm(List<string> issues, Dictionary<string, string> providerInfo)
|
||||
{
|
||||
providerInfo["Provider"] = "BouncyCastle";
|
||||
providerInfo["Implementation"] = "Managed .NET implementation";
|
||||
|
||||
// BouncyCastle should be available if the package is referenced
|
||||
providerInfo["Algorithms"] = "SM2, SM3, SM4";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Cryptography.Checks;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Cryptography;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin providing cryptography diagnostic checks including regional crypto,
|
||||
/// HSM connectivity, and licensing validation.
|
||||
/// </summary>
|
||||
public sealed class CryptographyPlugin : IDoctorPlugin
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string PluginId => "stellaops.doctor.cryptography";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName => "Cryptography";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorCategory Category => DoctorCategory.Cryptography;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version Version => new(1, 0, 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version MinEngineVersion => new(1, 0, 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAvailable(IServiceProvider services) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IDoctorCheck> GetChecks(DoctorPluginContext context) =>
|
||||
[
|
||||
new CryptoProviderAvailabilityCheck(),
|
||||
new FipsComplianceCheck(),
|
||||
new GostProviderCheck(),
|
||||
new SmProviderCheck(),
|
||||
new EidasProviderCheck(),
|
||||
new HsmConnectivityCheck(),
|
||||
new CryptoLicenseCheck(),
|
||||
new CryptoProCheck()
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct) => Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Cryptography.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering the Cryptography diagnostics plugin.
|
||||
/// </summary>
|
||||
public static class CryptographyPluginExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the Cryptography diagnostics plugin to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddDoctorCryptographyPlugin(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IDoctorPlugin, CryptographyPlugin>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user