Привет, Хабр!
Сегодня хочу рассказать про боль, которая, думаю, знакома многим, кто хоть раз сталкивался с поддержкой устаревших 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)
ссылка на оригинал статьи https://habr.com/ru/articles/1103226/