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

  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)

ссылка на оригинал статьи https://habr.com/ru/articles/1103226/