InquirerMt. Guiting-Guiting climbing activities suspended for three monthsThe Jerusalem PostAmericans begin voting in midterm election races with major implications for US-Israel policyBollywood HungamaAmit Rai exits OMG 3? Director and makers reportedly part ways over creative differencesDaily MaverickSTATES OF GOVERNANCE OP-ED: Can SA’s Public Service Amendment Act deliver better governance in Joburg’s Deep South?ZDF heuteAktuelle Pressemitteilungen des ZDFTechCrunchRivian spinout Also was working on an e-moto project in Kenya — until it walked awayIl Fatto QuotidianoMedaglie agli animali eroi, ma senza diritti: il paradosso del ddl della LegaFotogramasTim Burton, sobre Johnny Depp: "Que le guste el dinero o la fama es mentira, él no se convirtió en actor para ser glamuroso, sino para esconderse detrás de un personaje"E! OnlineHow Hoda Kotb and Joel Schiffman Remain Friendly ExesThe Hollywood Reporter‘Starlog’ Magazine Returns with First Cover in 17 Years7sur7Schaerbeek introduit un recours contre le permis du projet immobilier “Carré des Chardons”EuronewsTents return to Lisbon square after Portugal protest victory
The Daily Newsstand · Free, Always
Tuesday, November 3, 2026

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

Translate

Сегодня хочу рассказать про боль, которая, думаю, знакома многим, кто хоть раз сталкивался с поддержкой устаревших 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, к счастью, хранит всё в открытом доступе. Есть два файла:

  1. authrootstl.cab — CAB-архив со списком доверенных сертификатов (файл .stl, он же CTL).

  2. <thumbprint>.crt — Сами сертификаты по их SHA-1 отпечаткам.

То есть весь алгоритм сводится к четырём шагам:

  1. Скачать authrootstl.cab.

  2. Распаковать его (внутри лежит authroot.stl).

  3. Распарсить CTL и вытащить SHA-1 отпечатки всех сертификатов.

  4. Скачать недостающие сертификаты и записать их в реестр в нужном формате.

Технические детали

Парсинг 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)

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.