Лечим ошибки SSL на старых Windows: пишем утилиту обновления корневых сертификатов на C#

Сегодня хочу рассказать про боль, которая, думаю, знакома многим, кто хоть раз сталкивался с поддержкой устаревших Windows-систем. А также про то, как небольшая консольная утилита на C# решила проблему, которая годами заставляла меня и моих пользователей хвататься за голову.
С чего всё началось
Всё началось с проекта BELPORTAL — набора решений для работы с государственными порталами Беларуси. Среди прочего там есть клиент для доступа к порталам, и вот с ним-то и возникла проблема.
Пользователи ставили BELPORTAL на Windows 7, Windows XP, на терминальные серверы — словом, туда, где всё ещё жил старый добрый софт. И примерно через одного получали вот такую картину:
Не удается установить защищенное соединение с сервером
или на более свежих системах:
Ошибка 0x800B0101
или классическое:
Сертификат сервера не является доверенным
При этом HTTP-сайты открывались, интернет работал, антивирус молчал. Но HTTPS — ни в какую.
Диагностика: в чём оказалась проблема
Копнув глубже, я выяснил банальную вещь: устаревшее хранилище корневых сертификатов. Windows хранит список доверенных корневых сертификатов в реестре по пути:
HKLM\SOFTWARE\Microsoft\SystemCertificates\ROOT\Certificates
Microsoft регулярно обновляет этот список через Windows Update. Проблема в том, что:
На Windows XP Windows Update для сертификатов уже давно не работает.
На Windows 7 обновления сертификатов прекратились с окончанием поддержки.
На корпоративных машинах политиками часто отключён Windows Update целиком.
На терминальных серверах обновление сертификатов — это отдельный квест.
На изолированных системах (POS-терминалы, банкоматы, медицинское оборудование) интернета нет вообще.
В результате в системе хранится «замороженный» набор сертификатов примерно 2014–2017 года. А сайты с тех пор давно перешли на новые УЦ и новые цепочки сертификации. Отсюда и ошибки.
Что я решил сделать
Идея простая до безобразия: скачать актуальный список доверенных корневых сертификатов напрямую с серверов Microsoft и установить их в реестр, минуя Windows Update.
Microsoft, к счастью, хранит всё в открытом доступе. Есть два файла:
authrootstl.cab — CAB-архив со списком доверенных сертификатов (файл .stl, он же CTL).
<thumbprint>.crt — Сами сертификаты по их SHA-1 отпечаткам.
То есть весь алгоритм сводится к четырём шагам:
Скачать authrootstl.cab.
Распаковать его (внутри лежит authroot.stl).
Распарсить CTL и вытащить SHA-1 отпечатки всех сертификатов.
Скачать недостающие сертификаты и записать их в реестр в нужном формате.
Технические детали
Парсинг CTL
Самая нетривиальная часть. .stl — это Certificate Trust List, по сути ASN.1-структура с вложенными SEQUENCE. SHA-1 отпечатки хранятся как OCTET STRING длиной 20 байт внутри вложенных SEQUENCE.
Вот упрощённая версия парсера из утилиты (выдели этот код и нажми кнопку </> в редакторе):
static List<string> ParseCtlThumbprints(byte[] stlData)
{
var thumbprints = new List<string>();
int offset = 0;
while (offset < stlData.Length - 30)
{
if (stlData[offset] != 0x30) { offset++; continue; }
// Разбираем длину SEQUENCE (поддерживаем short и long form)
int seqLen, dataOffset;
if ((stlData[offset + 1] & 0x80) != 0)
{
int lenBytes = stlData[offset + 1] & 0x7F;
seqLen = 0;
for (int i = 0; i < lenBytes; i++)
seqLen = (seqLen << 8) | stlData[offset + 2 + i];
dataOffset = offset + 2 + lenBytes;
}
else
{
seqLen = stlData[offset + 1];
dataOffset = offset + 2;
}
// Внутри ищем вложенный SEQUENCE с OCTET STRING длиной 20 байт
// ... (проверки опущены для краткости, полный код в конце статьи)
offset = dataOffset + seqLen;
}
return thumbprints;
}
Плюс есть запасной метод FindSha1Hashes, который просто сканирует файл, ищет паттерн 0x04 0x14 (OCTET STRING длиной 20) и проверяет энтропию хеша — на случай, если ASN.1-парсер не справится с каким-то экзотическим вариантом .stl.
Многопоточная загрузка
Сертификатов в списке обычно порядка 130 штук. Скачивать их по одному — долго. Поэтому утилита разбивает список на чанки и запускает 16 потоков, каждый из которых качает свою порцию:
int threadCount = Math.Min(ThreadCount, total);
int chunkSize = (total + threadCount - 1) / threadCount;
for (int t = 0; t < threadCount; t++)
{
int start = t * chunkSize;
int end = Math.Min(start + chunkSize, total);
var thread = new Thread(() =>
{
for (int i = start; i < end; i++)
{
string thumb = needToDownload[i];
byte[] data = DownloadBytes(string.Format(CertCdnUrl, thumb));
lock (resultsLock)
{
results[thumb] = data;
completed++;
WriteProgress($"Загрузка сертификатов: {completed}/{total}");
}
}
});
thread.Start();
}
В результате время работы утилиты сокращается до 1–3 минут даже на слабых машинах.
Запись в реестр
Тут есть один неочевидный момент. Формат значения Blob — это не просто DER-байты сертификата. Это структура:
DWORD dwCertEncodingType = 0x20 (X509_ASN_ENCODING)
DWORD cbCertEncoded = 1
DWORD cbCertEncoded = длина DER
BYTE[] pbCertEncoded = DER-байты
Соответствующий код:
static void WriteCertToRegistry(string thumbprintUpper, byte[] derBytes)
{
string keyPath = @"SOFTWARE\Microsoft\SystemCertificates\ROOT\Certificates" + thumbprintUpper;
byte[] blob = new byte[12 + derBytes.Length];
Buffer.BlockCopy(BitConverter.GetBytes(0x20), 0, blob, 0, 4);
Buffer.BlockCopy(BitConverter.GetBytes(1), 0, blob, 4, 4);
Buffer.BlockCopy(BitConverter.GetBytes(derBytes.Length), 0, blob, 8, 4);
Buffer.BlockCopy(derBytes, 0, blob, 12, derBytes.Length);
using (RegistryKey key = Registry.LocalMachine.CreateSubKey(keyPath))
{
key.SetValue("Blob", blob, RegistryValueKind.Binary);
}
}
Оптимизация: ставим только недостающее
Очень быстро пришло понимание: если ставить все сертификаты каждый раз — это медленно и некрасиво. Поэтому утилита сначала читает уже установленные отпечатки из реестра и качает только недостающие:
var installed = GetInstalledThumbprints();
foreach (var thumb in thumbprints)
{
if (installed.Contains(thumb.ToLower()))
alreadyInstalled++;
else
needToDownload.Add(thumb);
}
На системах, где уже что-то обновлялось, разница может быть в разы.
Что видит пользователь
Я не стал делать GUI — незачем. Всё в консоли, но с цветным выводом, рамками и понятным прогрессом:
╔══════════════════════════════════════════════════════════╗
║ ║
║ BELPORTAL ║
║ Обновление корневых сертификатов v1.1 ║
║ ║
╚══════════════════════════════════════════════════════════╝
─── ДОВЕРЕННЫЕ КОРНЕВЫЕ СЕРТИФИКАТЫ ───
ℹ Загрузка списка доверенных сертификатов...
✔ Скачан authroot.cab (45 678 байт)
✔ Извлечен: authroot.stl
ℹ Найдено 128 сертификатов в списке Microsoft
ℹ Уже установлено: 12, требуется загрузить: 116
ℹ Загрузка 116 недостающих сертификатов...
⏳ Загрузка сертификатов: 116/116
✔ Загружено 116/116 сертификатов
ℹ Установка сертификатов в реестр...
─── ИТОГИ ───
✔ Уже было установлено: 12
✔ Установлено новых: 116
Запустил от администратора, дождался завершения, перезагрузился. Всё.
Что насчёт безопасности
Естественный вопрос: «А не подсунет ли мне эта утилита левые сертификаты?»
Отвечаю честно:
Утилита скачивает только с официальных серверов Microsoft.
Список сертификатов (CTL) подписан Microsoft, но утилита, если честно, не проверяет подпись CTL — она опирается на транспортную безопасность загрузки с официального домена. Это осознанный компромисс ради совместимости с Windows XP, где проверка подписи CTL средствами .NET 2.0 — это отдельная боль.
Никакой телеметрии, никакой отправки данных наружу.
Если для вас это критично — вы можете легко добавить проверку подписи CTL на своей стороне, исходники открыты.
Кому это нужно
Утилита реально выручает в таких сценариях:
Старые Windows XP / 7 / 8 без обновлений.
Корпоративные ПК с заблокированным Windows Update.
Терминальные серверы и RDS.
POS-терминалы, банкоматы, киоски.
Медицинское и промышленное оборудование с Windows Embedded.
Изолированные системы, куда сертификаты нужно принести «на флешке».
«А почему не использовать готовое?»
Резонный вопрос. Готовые решения есть, но:
certutil -generateSSTFromWU — есть только начиная с Windows 10 / Server 2016.
rootsupd.exe от Microsoft — обновлялся последний раз в районе 2013 года, сертификатов там мало и они устаревшие.
Ручное обновление через Windows Update — на XP и 7 уже не работает.
Так что ниша пустовала, а теперь — занята.
Где взять
Утилита распространяется бесплатно как портативный EXE (~100 КБ), не требует .NET Framework выше 2.0 (а он есть даже на XP) и работает как на x86, так и на x64.
Страница загрузки: https://belportal.by/utilita-obnovleniya-kornevyh-sertifikatov-belportal
Полный исходный код утилиты:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using Microsoft.Win32;
namespace UpdateRootCertificates
{
class Program
{
// --- Константы ---
private static readonly string LogFile = Path.Combine(Environment.GetEnvironmentVariable("TEMP") ?? "C:\", "UpdateRootCertificates.log");
private static readonly object LogLock = new object();
private static readonly string AuthRootCabUrl = "http://ctldl.windowsupdate.com/msdownload/update/v3/static/trustedr/en/authrootstl.cab";
private static readonly string CertCdnUrl = "http://ctldl.windowsupdate.com/msdownload/update/v3/static/trustedr/en/{0}.crt";
private static readonly string RootRegPath = @"SOFTWARE\Microsoft\SystemCertificates\ROOT\Certificates";
private static readonly int ThreadCount = 16;
private static bool Verbose = false;
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
foreach (var arg in args)
{
if (arg == "-v" arg == "--verbose")
Verbose = true;
if (arg == "-V" arg == "--version")
{
Console.WriteLine("UpdateRootCertificates v1.1");
return;
}
}
PrintHeader();
try
{
Log("=== СТАРТ ===");
WriteSuccess("Инициализация...");
string workDir = Path.Combine(Path.GetTempPath(), "UpdateRootCertificates_tmp");
if (Directory.Exists(workDir))
Directory.Delete(workDir, true);
Directory.CreateDirectory(workDir);
WriteInfo($"Рабочая папка: {workDir}");
try
{
ProcessAuthRoot(workDir);
}
finally
{
if (Directory.Exists(workDir))
Directory.Delete(workDir, true);
Log("Рабочая папка удалена");
}
Log("ГОТОВО");
WriteSuccess("\n✓ Операция успешно завершена!");
WriteInfo("Рекомендуется перезагрузить компьютер для применения изменений.");
WriteInfo("Файл лога: {LogFile}"); } catch (Exception ex) { Log("КРИТИЧЕСКАЯ ОШИБКА: {ex.Message}");
WriteError("\n✗ КРИТИЧЕСКАЯ ОШИБКА: {ex.Message}"); WriteInfo("Файл лога: {LogFile}");
}
if (Environment.UserInteractive)
{
Console.WriteLine();
WriteInfo("Нажмите любую клавишу для выхода...");
Console.ReadKey();
}
}
static void PrintHeader()
{
Console.Clear();
string title = "BELPORTAL";
string subtitle = "Обновление корневых сертификатов v1.1";
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("╔newstring(′═′,58)╗");Console.WriteLine("╔newstring(′═′,58)╗");Console.WriteLine("║{new string(' ', 58)}║");
Console.ForegroundColor = ConsoleColor.Yellow;
int leftPadding = (58 - title.Length) / 2;
Console.WriteLine($"║{new string(' ', leftPadding)}{title}{new string(' ', 58 - leftPadding - title.Length)}║");
Console.ForegroundColor = ConsoleColor.White;
leftPadding = (58 - subtitle.Length) / 2;
Console.WriteLine($"║{new string(' ', leftPadding)}{subtitle}{new string(' ', 58 - leftPadding - subtitle.Length)}║");
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine(║║"╚{new string('═', 58)}╝");
Console.ResetColor();
Console.WriteLine();
}
static void WriteInfo(string message)
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine($" ℹ {message}");
Console.ResetColor();
}
static void WriteSuccess(string message)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($" ✔ {message}");
Console.ResetColor();
}
static void WriteError(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($" ✗ {message}");
Console.ResetColor();
}
static void WriteWarning(string message)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" ⚠ {message}");
Console.ResetColor();
}
static void WriteProgress(string message)
{
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.Write($"\r ⏳ {message}");
Console.ResetColor();
}
static void WriteSection(string message)
{
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine($"\n ─── {message} ───");
Console.ResetColor();
}
static void Log(string message)
{
lock (LogLock)
{
try { File.AppendAllText(LogFile, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}{Environment.NewLine}"); }
catch { }
}
}
static void DownloadFile(string url, string destPath, int retries = 3)
{
WriteProgress($"Скачивание {Path.GetFileName(destPath)}...");
for (int attempt = 1; attempt <= retries; attempt++)
{
try
{
using (WebClient client = new WebClient())
{
client.DownloadFile(url, destPath);
}
if (File.Exists(destPath) && new FileInfo(destPath).Length > 0)
{
WriteSuccess("Скачан {Path.GetFileName(destPath)} ({new FileInfo(destPath).Length:N0} байт)"); return; } } catch (Exception ex) { if (attempt < retries) { WriteWarning("Попытка {attempt} не удалась, повтор...");
Thread.Sleep(1000);
}
else
{
throw new Exception($"Не удалось скачать {url}: {ex.Message}");
}
}
}
}
static byte[] DownloadBytes(string url)
{
try
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Timeout = 30000;
request.ReadWriteTimeout = 30000;
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
using (Stream stream = response.GetResponseStream())
{
using (MemoryStream ms = new MemoryStream())
{
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, bytesRead);
}
return ms.ToArray();
}
}
}
}
catch
{
return null;
}
}
static string ExtractCab(string cabPath, string destDir)
{
if (!Directory.Exists(destDir))
Directory.CreateDirectory(destDir);
WriteProgress("Извлечение CAB архива...");
string expandPath = Path.Combine(Environment.SystemDirectory, "expand.exe");
if (!File.Exists(expandPath))
throw new Exception("expand.exe не найден в системной папке");
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = expandPath,
Arguments = $""{cabPath}" -F:* "{destDir}"",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
using (Process process = Process.Start(psi))
{
process.WaitForExit();
if (process.ExitCode != 0)
throw new Exception($"expand.exe завершился с кодом {process.ExitCode}");
}
string[] files = Directory.GetFiles(destDir);
if (files.Length == 0)
throw new Exception("Из CAB архива не извлечено ни одного файла");
string extracted = files[0];
if (!extracted.ToLower().EndsWith(".stl"))
{
string renamed = Path.ChangeExtension(extracted, ".stl");
File.Move(extracted, renamed);
extracted = renamed;
}
WriteSuccess($"Извлечен: {Path.GetFileName(extracted)}");
return extracted;
}
// --- ПРАВИЛЬНЫЙ ПАРСЕР CTL (работает с реальным STL) ---
static List<string> ParseCtlThumbprints(byte[] stlData)
{
var thumbprints = new List<string>();
try
{
int offset = 0;
while (offset < stlData.Length - 30)
{
if (stlData[offset] == 0x30)
{
int seqLen;
int dataOffset;
if ((stlData[offset + 1] & 0x80) != 0)
{
int lenBytes = stlData[offset + 1] & 0x7F;
if (lenBytes > 0 && offset + 1 + lenBytes < stlData.Length)
{
seqLen = 0;
for (int i = 0; i < lenBytes; i++)
{
seqLen = (seqLen << 8) | stlData[offset + 2 + i];
}
dataOffset = offset + 2 + lenBytes;
}
else
{
offset++;
continue;
}
}
else
{
seqLen = stlData[offset + 1];
dataOffset = offset + 2;
}
if (dataOffset + seqLen > stlData.Length || seqLen < 22)
{
offset++;
continue;
}
int innerOffset = dataOffset;
if (innerOffset + 2 < stlData.Length && stlData[innerOffset] == 0x30)
{
int innerLen;
int innerDataOffset;
if ((stlData[innerOffset + 1] & 0x80) != 0)
{
int lenBytes = stlData[innerOffset + 1] & 0x7F;
if (lenBytes > 0 && innerOffset + 1 + lenBytes < stlData.Length)
{
innerLen = 0;
for (int i = 0; i < lenBytes; i++)
{
innerLen = (innerLen << 8) | stlData[innerOffset + 2 + i];
}
innerDataOffset = innerOffset + 2 + lenBytes;
}
else
{
offset++;
continue;
}
}
else
{
innerLen = stlData[innerOffset + 1];
innerDataOffset = innerOffset + 2;
}
if (innerDataOffset + 22 <= stlData.Length &&
stlData[innerDataOffset] == 0x04 &&
stlData[innerDataOffset + 1] == 0x14)
{
byte[] hash = new byte[20];
Array.Copy(stlData, innerDataOffset + 2, hash, 0, 20);
bool valid = false;
foreach (byte b in hash)
{
if (b != 0 && b != 0xFF)
{
valid = true;
break;
}
}
if (valid)
{
string thumbprint = BitConverter.ToString(hash).Replace("-", "").ToLower();
if (!thumbprints.Contains(thumbprint))
{
thumbprints.Add(thumbprint);
}
}
}
}
offset = dataOffset + seqLen;
}
else
{
offset++;
}
}
if (thumbprints.Count == 0)
{
thumbprints = FindSha1Hashes(stlData);
}
}
catch (Exception ex)
{
WriteWarning($"Ошибка при парсинге CTL: {ex.Message}");
thumbprints = FindSha1Hashes(stlData);
}
return thumbprints;
}
// --- ЗАПАСНОЙ МЕТОД: поиск SHA-1 хешей напрямую ---
static List<string> FindSha1Hashes(byte[] data)
{
var thumbprints = new List<string>();
for (int i = 0; i <= data.Length - 22; i++)
{
if (data[i] == 0x04 && data[i + 1] == 0x14)
{
int hashStart = i + 2;
byte[] potentialHash = new byte[20];
Array.Copy(data, hashStart, potentialHash, 0, 20);
int zeroCount = 0;
int ffCount = 0;
int uniqueCount = 0;
var seen = new HashSet<byte>();
foreach (byte b in potentialHash)
{
if (b == 0) zeroCount++;
if (b == 0xFF) ffCount++;
if (!seen.Contains(b)) { seen.Add(b); uniqueCount++; }
}
if (zeroCount < 10 && ffCount < 10 && uniqueCount > 5)
{
string thumbprint = BitConverter.ToString(potentialHash).Replace("-", "").ToLower();
if (!thumbprints.Contains(thumbprint))
{
thumbprints.Add(thumbprint);
}
}
}
}
return thumbprints;
}
static HashSet<string> GetInstalledThumbprints()
{
var installed = new HashSet<string>();
try
{
string regPath = @"SOFTWARE\Microsoft\SystemCertificates\ROOT\Certificates";
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(regPath))
{
if (key != null)
{
foreach (string subKeyName in key.GetSubKeyNames())
{
installed.Add(subKeyName.ToLower());
}
}
}
}
catch { }
return installed;
}
static void WriteCertToRegistry(string thumbprintUpper, byte[] derBytes)
{
string keyPath = RootRegPath + "\" + thumbprintUpper;
byte[] blob = new byte[12 + derBytes.Length];
Buffer.BlockCopy(BitConverter.GetBytes(0x20), 0, blob, 0, 4);
Buffer.BlockCopy(BitConverter.GetBytes(1), 0, blob, 4, 4);
Buffer.BlockCopy(BitConverter.GetBytes(derBytes.Length), 0, blob, 8, 4);
Buffer.BlockCopy(derBytes, 0, blob, 12, derBytes.Length);
using (RegistryKey key = Registry.LocalMachine.CreateSubKey(keyPath))
{
key.SetValue("Blob", blob, RegistryValueKind.Binary);
}
}
static void ProcessAuthRoot(string workDir)
{
WriteSection("ДОВЕРЕННЫЕ КОРНЕВЫЕ СЕРТИФИКАТЫ");
string cabPath = Path.Combine(workDir, "authroot.cab");
string stlDir = Path.Combine(workDir, "authroot");
WriteInfo("Загрузка списка доверенных сертификатов...");
DownloadFile(AuthRootCabUrl, cabPath);
string stlPath = ExtractCab(cabPath, stlDir);
byte[] stlData = File.ReadAllBytes(stlPath);
List<string> thumbprints = ParseCtlThumbprints(stlData);
if (thumbprints.Count == 0)
{
WriteWarning("Не найдено сертификатов в списке. Проверьте файл authroot.stl.");
return;
}
WriteInfo($"Найдено {thumbprints.Count} сертификатов в списке Microsoft");
WriteInfo("Проверка установленных сертификатов...");
var installed = GetInstalledThumbprints();
var needToDownload = new List<string>();
int alreadyInstalled = 0;
foreach (var thumb in thumbprints)
{
if (installed.Contains(thumb.ToLower()))
{
alreadyInstalled++;
if (Verbose)
WriteInfo($"Уже установлен: {thumb.ToUpper()}");
}
else
{
needToDownload.Add(thumb);
}
}
WriteInfo($"Уже установлено: {alreadyInstalled}, требуется загрузить: {needToDownload.Count}");
if (needToDownload.Count == 0)
{
WriteSuccess("Все сертификаты уже установлены. Ничего не делаем.");
return;
}
int total = needToDownload.Count;
var results = new Dictionary<string, byte[]>();
var resultsLock = new object();
int completed = 0;
WriteInfo($"Загрузка {total} недостающих сертификатов...");
var threads = new List<Thread>();
int threadCount = Math.Min(ThreadCount, total);
int chunkSize = (total + threadCount - 1) / threadCount;
for (int t = 0; t < threadCount; t++)
{
int start = t * chunkSize;
int end = Math.Min(start + chunkSize, total);
if (start >= end) break;
var thread = new Thread(() =>
{
for (int i = start; i < end; i++)
{
string thumb = needToDownload[i];
byte[] data = DownloadBytes(string.Format(CertCdnUrl, thumb));
lock (resultsLock)
{
results[thumb] = data;
completed++;
WriteProgress($"Загрузка сертификатов: {completed}/{total}");
}
}
});
thread.Start();
threads.Add(thread);
}
foreach (var thread in threads)
thread.Join();
Console.WriteLine();
WriteSuccess($"Загружено {results.Count}/{total} сертификатов");
WriteInfo("Установка сертификатов в реестр...");
int regAdded = 0;
int regFailed = 0;
foreach (var kvp in results)
{
string thumb = kvp.Key;
byte[] der = kvp.Value;
if (der == null) { regFailed++; continue; }
try
{
WriteCertToRegistry(thumb.ToUpper(), der);
regAdded++;
if (Verbose)
WriteSuccess("Установлен: {thumb.ToUpper()}"); } catch (Exception ex) { WriteError("Ошибка установки {thumb.ToUpper()}: {ex.Message}");
regFailed++;
}
}
int miss = total - results.Count;
WriteSection("ИТОГИ");
WriteSuccess("✔Ужебылоустановлено:alreadyInstalled");WriteSuccess("✔Ужебылоустановлено:alreadyInstalled");WriteSuccess("✔ Установлено новых: {regAdded}");
if (regFailed > 0) WriteError("✗Ошибокустановки:regFailed");if(miss>0)WriteWarning("✗Ошибокустановки:regFailed");if(miss>0)WriteWarning("⚠ Недоступно на сервере: {miss}");
}
}
}
Итоги и планы
Что сделано:
Утилита, которая лечит SSL-ошибки на старых Windows.
Работает на XP–11, включая серверные редакции.
Не требует .NET выше 2.0.
Не требует установки.
Бесплатна.
Что в планах:
Поддержка проверки подписи CTL (для параноиков).
Режим «офлайн-пакет» — с возможностью скачать сертификаты на одной машине и установить на другой.
Опциональный GUI для тех, кто боится консоли.
Если у вас есть старые машины, которые вдруг перестали открывать HTTPS-сайты, или вы поддерживаете парк устаревших Windows — попробуйте. Буду рад фидбеку, баг-репортам и идеям по улучшению.
Ссылки:
🌐 Страница проекта (https://belportal.by/utilita-obnovleniya-kornevyh-sertifikatov-belportal)
💬 Telegram (https://t.me/BelPortal)
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.