552 lines
23 KiB
C#
552 lines
23 KiB
C#
using System.Collections;
|
||
using System.Reflection;
|
||
using System.Resources;
|
||
using System.Security.Cryptography.X509Certificates;
|
||
using System.Text;
|
||
|
||
namespace CertificateChecker;
|
||
|
||
/// <summary>
|
||
/// Основной класс для проверки сертификатов
|
||
/// </summary>
|
||
public class CertificateChecker
|
||
{
|
||
/// <summary>
|
||
/// Загружает сертификаты из Resources.resx
|
||
/// </summary>
|
||
public List<X509Certificate2> LoadCertificatesFromResources()
|
||
{
|
||
var certificates = new List<X509Certificate2>();
|
||
var assembly = Assembly.GetExecutingAssembly();
|
||
|
||
Console.WriteLine(" 📂 Поиск сертификатов в ресурсах...");
|
||
Console.WriteLine($" Сборка: {assembly.GetName().Name}");
|
||
|
||
try
|
||
{
|
||
// Получаем ВСЕ ресурсы из сборки
|
||
var resourceNames = assembly.GetManifestResourceNames();
|
||
|
||
Console.WriteLine($" Найдено ресурсов: {resourceNames.Length}");
|
||
|
||
if (resourceNames.Length == 0)
|
||
{
|
||
Console.WriteLine(" ⚠️ Ресурсы не найдены. Проверьте файл Resources.resx");
|
||
return certificates;
|
||
}
|
||
|
||
// Ищем ресурсы, которые содержат "Resources" в названии
|
||
var resourcesFile = resourceNames.FirstOrDefault(r => r.Contains("Resources"));
|
||
|
||
if (string.IsNullOrEmpty(resourcesFile))
|
||
{
|
||
Console.WriteLine(" ⚠️ Файл Resources.resx не найден");
|
||
Console.WriteLine($" Доступные ресурсы: {string.Join(", ", resourceNames)}");
|
||
return certificates;
|
||
}
|
||
|
||
Console.WriteLine($" Найден файл ресурсов: {resourcesFile}");
|
||
|
||
// Создаем ResourceManager для найденного файла
|
||
var resourceManagerName = Path.GetFileNameWithoutExtension(resourcesFile);
|
||
Console.WriteLine($" Имя ResourceManager: {resourceManagerName}");
|
||
|
||
var resourceManager = new ResourceManager(resourceManagerName, assembly);
|
||
|
||
// Получаем все ресурсы
|
||
var resourceSet = resourceManager.GetResourceSet(System.Globalization.CultureInfo.InvariantCulture, true, true);
|
||
|
||
if (resourceSet == null)
|
||
{
|
||
Console.WriteLine(" ❌ Не удалось загрузить ResourceSet");
|
||
return certificates;
|
||
}
|
||
|
||
// Собираем все ключи ресурсов
|
||
var keys = new List<string>();
|
||
foreach (DictionaryEntry entry in resourceSet)
|
||
{
|
||
keys.Add(entry.Key.ToString());
|
||
}
|
||
|
||
Console.WriteLine($" Найдено ключей в ресурсах: {keys.Count}");
|
||
Console.WriteLine($" Ключи: {string.Join(", ", keys)}");
|
||
|
||
// Фильтруем ключи, которые содержат "cert", "ca", "root", "sub"
|
||
var certKeys = keys
|
||
.Where(key => key.ToLower().Contains("cert") ||
|
||
key.ToLower().Contains("ca") ||
|
||
key.ToLower().Contains("root") ||
|
||
key.ToLower().Contains("sub"))
|
||
.ToList();
|
||
|
||
if (!certKeys.Any())
|
||
{
|
||
Console.WriteLine(" ⚠️ Сертификаты в ресурсах не найдены");
|
||
Console.WriteLine($" Ищите ключи содержащие: cert, ca, root, sub");
|
||
return certificates;
|
||
}
|
||
|
||
Console.WriteLine($"\n Найдено сертификатов: {certKeys.Count}");
|
||
|
||
foreach (var key in certKeys)
|
||
{
|
||
try
|
||
{
|
||
Console.WriteLine($"\n 📄 Загрузка: {key}");
|
||
|
||
// Получаем значение ресурса
|
||
var value = resourceManager.GetObject(key);
|
||
|
||
if (value == null)
|
||
{
|
||
Console.WriteLine($" ❌ Ресурс {key} пустой");
|
||
continue;
|
||
}
|
||
|
||
// Проверяем тип ресурса
|
||
if (value is byte[] bytes)
|
||
{
|
||
if (bytes.Length == 0)
|
||
{
|
||
Console.WriteLine($" ❌ Ресурс {key} пустой (0 байт)");
|
||
continue;
|
||
}
|
||
|
||
Console.WriteLine($" Размер: {bytes.Length} байт");
|
||
|
||
try
|
||
{
|
||
// Пробуем загрузить как сертификат
|
||
var certificate = new X509Certificate2(bytes);
|
||
|
||
// Устанавливаем понятное имя
|
||
if (string.IsNullOrEmpty(certificate.FriendlyName))
|
||
{
|
||
certificate.FriendlyName = key.Replace("_", " ").Trim();
|
||
}
|
||
|
||
certificates.Add(certificate);
|
||
|
||
Console.WriteLine($" ✅ Загружен сертификат: {certificate.FriendlyName}");
|
||
Console.WriteLine($" Субъект: {certificate.Subject}");
|
||
Console.WriteLine($" Отпечаток: {certificate.Thumbprint}");
|
||
Console.WriteLine($" Действителен до: {certificate.NotAfter:dd.MM.yyyy}");
|
||
Console.WriteLine($" Закрытый ключ: {(certificate.HasPrivateKey ? "✅ Есть" : "❌ Нет")}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($" ⚠️ Не удалось загрузить {key} как сертификат: {ex.Message}");
|
||
Console.WriteLine($" Возможно это не сертификат или требуется пароль");
|
||
|
||
// Пробуем с паролем для PFX
|
||
if (key.ToLower().Contains("pfx") || key.ToLower().Contains("p12") || key.ToLower().Contains("gost"))
|
||
{
|
||
Console.Write(" 🔑 Введите пароль для {0}: ", key);
|
||
var password = Console.ReadLine();
|
||
|
||
if (!string.IsNullOrEmpty(password))
|
||
{
|
||
try
|
||
{
|
||
var certificate = new X509Certificate2(bytes, password);
|
||
certificate.FriendlyName = key.Replace("_", " ").Trim();
|
||
certificates.Add(certificate);
|
||
|
||
Console.WriteLine($" ✅ Загружен с паролем: {certificate.FriendlyName}");
|
||
}
|
||
catch (Exception ex2)
|
||
{
|
||
Console.WriteLine($" ❌ Ошибка загрузки с паролем: {ex2.Message}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine($" ⚠️ Ресурс {key} имеет тип {value.GetType().Name}, ожидался byte[]");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($" ❌ Ошибка обработки {key}: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($" ❌ Общая ошибка: {ex.Message}");
|
||
Console.WriteLine($" {ex.StackTrace}");
|
||
}
|
||
|
||
Console.WriteLine($"\n ИТОГО загружено сертификатов: {certificates.Count}");
|
||
return certificates;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Проверяет, установлен ли сертификат на ПК
|
||
/// </summary>
|
||
public async Task<CertificateCheckResult> CheckCertificateInstalledAsync(X509Certificate2 certificate)
|
||
{
|
||
var result = new CertificateCheckResult
|
||
{
|
||
FriendlyName = certificate.FriendlyName,
|
||
Subject = certificate.Subject,
|
||
Issuer = certificate.Issuer,
|
||
SerialNumber = certificate.SerialNumber,
|
||
NotBefore = certificate.NotBefore,
|
||
NotAfter = certificate.NotAfter,
|
||
Thumbprint = certificate.Thumbprint,
|
||
IsInstalled = false,
|
||
FoundInStores = new List<string>(),
|
||
HasPrivateKey = certificate.HasPrivateKey
|
||
};
|
||
|
||
// Проверяем в разных хранилищах
|
||
var stores = new[]
|
||
{
|
||
(StoreName.My, StoreLocation.CurrentUser, "Личные (CurrentUser)"),
|
||
(StoreName.My, StoreLocation.LocalMachine, "Личные (LocalMachine)"),
|
||
(StoreName.Root, StoreLocation.CurrentUser, "Доверенные корневые (CurrentUser)"),
|
||
(StoreName.Root, StoreLocation.LocalMachine, "Доверенные корневые (LocalMachine)"),
|
||
(StoreName.TrustedPeople, StoreLocation.CurrentUser, "Доверенные лица (CurrentUser)"),
|
||
(StoreName.TrustedPeople, StoreLocation.LocalMachine, "Доверенные лица (LocalMachine)"),
|
||
(StoreName.CertificateAuthority, StoreLocation.CurrentUser, "Промежуточные (CurrentUser)"),
|
||
(StoreName.CertificateAuthority, StoreLocation.LocalMachine, "Промежуточные (LocalMachine)"),
|
||
(StoreName.AddressBook, StoreLocation.CurrentUser, "Адресная книга (CurrentUser)"),
|
||
(StoreName.AddressBook, StoreLocation.LocalMachine, "Адресная книга (LocalMachine)")
|
||
};
|
||
|
||
foreach (var (storeName, storeLocation, displayName) in stores)
|
||
{
|
||
try
|
||
{
|
||
using var store = new X509Store(storeName, storeLocation);
|
||
store.Open(OpenFlags.ReadOnly);
|
||
|
||
// Ищем по отпечатку
|
||
var foundCerts = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, false);
|
||
|
||
if (foundCerts.Count > 0)
|
||
{
|
||
result.IsInstalled = true;
|
||
result.StoreName = storeName;
|
||
result.StoreLocation = storeLocation;
|
||
result.FoundCertificate = foundCerts[0];
|
||
result.FoundInStores.Add(displayName);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// Некоторые хранилища могут быть недоступны
|
||
}
|
||
}
|
||
|
||
// Проверка цепочки сертификатов
|
||
result.ChainValidationResult = await ValidateCertificateChainAsync(certificate);
|
||
|
||
// Проверка срока действия
|
||
result.IsValid = DateTime.Now >= certificate.NotBefore &&
|
||
DateTime.Now <= certificate.NotAfter;
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Проверка цепочки сертификатов
|
||
/// </summary>
|
||
private async Task<ChainValidationResult> ValidateCertificateChainAsync(X509Certificate2 certificate)
|
||
{
|
||
var result = new ChainValidationResult();
|
||
|
||
try
|
||
{
|
||
using var chain = new X509Chain();
|
||
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
|
||
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
|
||
|
||
var isValid = chain.Build(certificate);
|
||
|
||
result.IsValid = isValid;
|
||
result.Status = chain.ChainStatus
|
||
.Select(s => new ChainStatusInfo
|
||
{
|
||
Status = s.Status,
|
||
StatusInformation = s.StatusInformation
|
||
})
|
||
.ToList();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.ErrorMessage = ex.Message;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Проверяет сертификат по отпечатку
|
||
/// </summary>
|
||
public bool IsCertificateInstalled(string thumbprint)
|
||
{
|
||
var stores = new[]
|
||
{
|
||
(StoreName.My, StoreLocation.CurrentUser),
|
||
(StoreName.My, StoreLocation.LocalMachine),
|
||
(StoreName.Root, StoreLocation.CurrentUser),
|
||
(StoreName.Root, StoreLocation.LocalMachine)
|
||
};
|
||
|
||
foreach (var (storeName, storeLocation) in stores)
|
||
{
|
||
try
|
||
{
|
||
using var store = new X509Store(storeName, storeLocation);
|
||
store.Open(OpenFlags.ReadOnly);
|
||
var certificates = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
|
||
if (certificates.Count > 0)
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
// Пропускаем недоступные хранилища
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Устанавливает сертификат в указанное хранилище
|
||
/// </summary>
|
||
public async Task<bool> InstallCertificateAsync(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation, string friendlyName = "")
|
||
{
|
||
try
|
||
{
|
||
Console.WriteLine($"\n 📥 Установка сертификата {friendlyName} в {storeLocation}\\{storeName}...");
|
||
|
||
using var store = new X509Store(storeName, storeLocation);
|
||
store.Open(OpenFlags.ReadWrite);
|
||
|
||
// Проверяем, не установлен ли уже
|
||
var existing = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, false);
|
||
if (existing.Count > 0)
|
||
{
|
||
Console.WriteLine($" ⚠️ Сертификат уже установлен в {storeLocation}\\{storeName}");
|
||
return true;
|
||
}
|
||
|
||
// Устанавливаем понятное имя
|
||
if (!string.IsNullOrEmpty(friendlyName))
|
||
{
|
||
certificate.FriendlyName = friendlyName;
|
||
}
|
||
|
||
store.Add(certificate);
|
||
store.Close();
|
||
|
||
Console.WriteLine($" ✅ Сертификат успешно установлен в {storeLocation}\\{storeName}");
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($" ❌ Ошибка установки: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Устанавливает сертификат с автоматическим выбором хранилища
|
||
/// </summary>
|
||
public async Task<InstallResult> InstallCertificateWithAutoStoreAsync(X509Certificate2 certificate)
|
||
{
|
||
var result = new InstallResult
|
||
{
|
||
Certificate = certificate,
|
||
Success = false,
|
||
Messages = new List<string>()
|
||
};
|
||
|
||
// Определяем куда устанавливать в зависимости от типа сертификата
|
||
var subject = certificate.Subject.ToLower();
|
||
var isRoot = subject.Contains("root") || subject.Contains("ca") && !subject.Contains("sub");
|
||
var isCA = subject.Contains("ca");
|
||
var isPersonal = subject.Contains("client") || subject.Contains("user") || subject.Contains("person");
|
||
|
||
List<(StoreName store, StoreLocation location, string description)> storesToInstall;
|
||
|
||
if (isRoot)
|
||
{
|
||
// Корневой сертификат
|
||
storesToInstall = new List<(StoreName, StoreLocation, string)>
|
||
{
|
||
(StoreName.Root, StoreLocation.LocalMachine, "Доверенные корневые (LocalMachine)"),
|
||
(StoreName.Root, StoreLocation.CurrentUser, "Доверенные корневые (CurrentUser)")
|
||
};
|
||
result.InstallType = "Корневой сертификат";
|
||
}
|
||
else if (isCA && !isRoot)
|
||
{
|
||
// Промежуточный сертификат
|
||
storesToInstall = new List<(StoreName, StoreLocation, string)>
|
||
{
|
||
(StoreName.CertificateAuthority, StoreLocation.LocalMachine, "Промежуточные (LocalMachine)"),
|
||
(StoreName.CertificateAuthority, StoreLocation.CurrentUser, "Промежуточные (CurrentUser)")
|
||
};
|
||
result.InstallType = "Промежуточный сертификат";
|
||
}
|
||
else if (isPersonal)
|
||
{
|
||
// Личный сертификат
|
||
storesToInstall = new List<(StoreName, StoreLocation, string)>
|
||
{
|
||
(StoreName.My, StoreLocation.CurrentUser, "Личные (CurrentUser)"),
|
||
(StoreName.My, StoreLocation.LocalMachine, "Личные (LocalMachine)")
|
||
};
|
||
result.InstallType = "Личный сертификат";
|
||
}
|
||
else
|
||
{
|
||
// Неопределенный тип - устанавливаем в личные
|
||
storesToInstall = new List<(StoreName, StoreLocation, string)>
|
||
{
|
||
(StoreName.My, StoreLocation.CurrentUser, "Личные (CurrentUser)")
|
||
};
|
||
result.InstallType = "Неопределенный тип (личный)";
|
||
}
|
||
|
||
Console.WriteLine($"\n 📌 Тип сертификата: {result.InstallType}");
|
||
Console.WriteLine($" 📌 Субъект: {certificate.Subject}");
|
||
|
||
// Устанавливаем в каждое хранилище
|
||
foreach (var (store, location, description) in storesToInstall)
|
||
{
|
||
try
|
||
{
|
||
var success = await InstallCertificateAsync(certificate, store, location, certificate.FriendlyName);
|
||
if (success)
|
||
{
|
||
result.Success = true;
|
||
result.InstalledStores.Add($"{location}\\{store}");
|
||
result.Messages.Add($"✅ Установлен в {description}");
|
||
}
|
||
else
|
||
{
|
||
result.Messages.Add($"❌ Не удалось установить в {description}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.Messages.Add($"❌ Ошибка при установке в {description}: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// Если не удалось установить никуда, пробуем в CurrentUser
|
||
if (!result.Success)
|
||
{
|
||
Console.WriteLine($" ⚠️ Пробуем установить в CurrentUser...");
|
||
try
|
||
{
|
||
var success = await InstallCertificateAsync(certificate, StoreName.My, StoreLocation.CurrentUser, certificate.FriendlyName);
|
||
if (success)
|
||
{
|
||
result.Success = true;
|
||
result.InstalledStores.Add($"CurrentUser\\My");
|
||
result.Messages.Add($"✅ Установлен в Личные (CurrentUser)");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.Messages.Add($"❌ Ошибка при установке в CurrentUser: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Удаляет сертификат из указанного хранилища
|
||
/// </summary>
|
||
public async Task<bool> DeleteCertificateAsync(string thumbprint, StoreName storeName, StoreLocation storeLocation)
|
||
{
|
||
try
|
||
{
|
||
Console.WriteLine($"\n 🗑️ Удаление сертификата из {storeLocation}\\{storeName}...");
|
||
|
||
using var store = new X509Store(storeName, storeLocation);
|
||
store.Open(OpenFlags.ReadWrite);
|
||
|
||
var certificates = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
|
||
if (certificates.Count == 0)
|
||
{
|
||
Console.WriteLine($" ⚠️ Сертификат не найден в {storeLocation}\\{storeName}");
|
||
return false;
|
||
}
|
||
|
||
store.Remove(certificates[0]);
|
||
store.Close();
|
||
|
||
Console.WriteLine($" ✅ Сертификат удален из {storeLocation}\\{storeName}");
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($" ❌ Ошибка удаления: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Результат проверки сертификата
|
||
/// </summary>
|
||
public class CertificateCheckResult
|
||
{
|
||
public string FriendlyName { get; set; } = string.Empty;
|
||
public string Subject { get; set; } = string.Empty;
|
||
public string Issuer { get; set; } = string.Empty;
|
||
public string SerialNumber { get; set; } = string.Empty;
|
||
public DateTime NotBefore { get; set; }
|
||
public DateTime NotAfter { get; set; }
|
||
public string Thumbprint { get; set; } = string.Empty;
|
||
public bool IsInstalled { get; set; }
|
||
public bool IsValid { get; set; }
|
||
public bool HasPrivateKey { get; set; }
|
||
public StoreName StoreName { get; set; }
|
||
public StoreLocation StoreLocation { get; set; }
|
||
public X509Certificate2? FoundCertificate { get; set; }
|
||
public List<string> FoundInStores { get; set; } = new();
|
||
public ChainValidationResult ChainValidationResult { get; set; } = new();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Результат установки сертификата
|
||
/// </summary>
|
||
public class InstallResult
|
||
{
|
||
public X509Certificate2 Certificate { get; set; } = null!;
|
||
public bool Success { get; set; }
|
||
public string InstallType { get; set; } = string.Empty;
|
||
public List<string> InstalledStores { get; set; } = new();
|
||
public List<string> Messages { get; set; } = new();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Результат проверки цепочки сертификатов
|
||
/// </summary>
|
||
public class ChainValidationResult
|
||
{
|
||
public bool IsValid { get; set; }
|
||
public string? ErrorMessage { get; set; }
|
||
public List<ChainStatusInfo> Status { get; set; } = new();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Информация о статусе цепочки
|
||
/// </summary>
|
||
public class ChainStatusInfo
|
||
{
|
||
public X509ChainStatusFlags Status { get; set; }
|
||
public string? StatusInformation { get; set; }
|
||
} |