В комментариях к моей недавней статье про фейл с ГОСТ сертификатом сайта ГосУслуг возникла дискуссия о доверии к российским корневым сертификатам Минцифры и о рисках их установки (в систему, в отдельный браузер, или риск использования Яндекс браузера, где они встроены).
Здесь вдаваться в подробности и повторять сказанное много раз не буду, включу только некое саммари из основных тезисов для тех, кто не погружен в тему:
-
Сами корневые сертификаты условно безопасны
Извините, этот пункт для домохозяек — сами сертификаты ваш компьютер не взломают. Установка их в доверенное хранилище открывает путь к потенциальным атакам (см. ниже).
-
Если вы их поместили в доверенное хранилище, потенциально ваш трафик определенные лица могут расшифровать и даже подделать
Пояснения: лица, имеющие доступ к трафику (например, через печально известные ТСПУ), могут с помощью сертификата промежуточного УЦ (подписанного корневым) выпустить в онлайн режиме поддельный сертификат для сайта, с которым вы намереваетесь связаться — допустим, gmail.com или telegram.org. Это называется MitM-атака, на промежуточном сетевом узле трафик можно читать или изменять по желанию атакующего.
Хоть и не хочу ссылаться на этого нейрослопового школотрона (zarazaexe), но свежее и доходчивее ничего не нашел — почитайте.
-
Защититься от такой атаки сложно
Можно попытаться ограничить потенциальный ущерб. Скажем, установить корневые отечественные сертификаты в отдельный браузер или профиль и использовать его только, чтобы заходить на российские сайты, на которые иначе зайти нельзя.
Также с помощью диких костылей (уж простите, @outlingo) можно ограничить зону действия корневых сертификатов (скажем, до *.ru).
-
Certificate Transparency логи и Яндекс браузер — неплохое решение в плане безопасности
Про Certificate Transparency можно почитать тут (ну или в википедии). В крации: запись о выданном сертификате включается в типа-блокчейн, а в сам серт — пометки об этом.
У меня мало хороших слов в адрес Яндекс браузера и Яндекса вообще, но тут им надо отдать должное: они инициировали и продавили решение все выпускаемые отечественные сертификаты записывать в CT-логи (https://habr.com/ru/companies/yandex/articles/667300/).
Яндекс браузер (если не врёт) без записи в CT-логе сертификат не примет. Это очень осложняет онлайн MitM-атаки и оставляет следы от попыток.
Другие браузеры при добавлении внешних корневых сертификатов проверку CT-логов для соответствующих цепочек отключают (ну типа думают, что они в корпоративной среде).
Цель этой статьи — кратко рассказать об CT-логах для отечественных сертификатов и инструментах работы с ними.
CT-лог отечественный
Ну собственно их три:
Yandex Agate Log
VK NCA Log
The Ministry of Digital Development and Communications Log
(может есть и другие, но Яндекс доверяет только этим).
Тут список доверенных логов Яндекс браузера со ссылками и хешами ключей в формате JSON — https://browser-resources.s3.yandex.net/ctlog/ctlog.json.
Несколько неприятный факт состоит в том, что ссылки на логи не статичны, и их обновляют каждый год.
Инструменты для работы с CT-логами
То, что Яндекс проверяет наличие записи в логах, конечно, хорошо... А вот как бы самому руками все это пощупать?
Сами логи устроены максимально человеконенавистнически, и просто проверить по ссылочке, все ли хорошо у сертификата с включением в лог, нельзя.
Вообще, добрые люди придумали сервисы мониторинга (наподобие визуализаторов блокчейна), которые парсят эти логи и позволяют поискать и покликать. Но глобальные сервисы с российскими логами не работают (а самый известный https://crt.sh/ — так по-моему вообще сдох). Есть отечественный аналог — https://ct.tlscc.ru/. И снова "но" — похоже, что он тоже слегка заброшен: так, некоторых сертификатов (например, актуальный для db.vtb.ru) я там не нашел, и вообще, кажется, что он не работал с новыми адресами CT-логов 2027 года (см. выше, например: https://ct-agate.yandex.net/2027/).
Так что я, смирив стыд, помучил ChatGPT и родил от него следующий скрипт для проверки сертификатов по всем российским логам:
Код (убрал под спойлер)
#!/usr/bin/env python3 import sys import json import base64 import hashlib import struct import requests from asn1crypto import x509, pem # ============================================================ # Yandex CT log list # Static snapshot: # version: 1.229 # log_list_timestamp: 2026-08-12T22:00:21Z # ============================================================ REMOTE_LOG_LIST_URL = \ "https://browser-resources.s3.yandex.net/ctlog/ctlog.json" STATIC_LIST_VERSION = "1.229" STATIC_LIST_TIMESTAMP = "2026-08-12T22:00:21Z" LOGS = [ { "operator": "Yandex", "description": "Yandex Agate-2026 log", "url": "https://ct-agate.yandex.net/2026/", "log_id": "Vh7HRN8a9wLmJoOt38SVz3WjGwxs6KDH6gEDMJC3SIU=", "start": "2026-01-01T00:00:00Z", "end": "2027-01-01T00:00:00Z", }, { "operator": "Yandex", "description": "Yandex Agate-2027 log", "url": "https://ct-agate.yandex.net/2027/", "log_id": "GukWh2bX94qcsIb4xJw8Xc64yZMAqa9L/7CbB0+BLBc=", "start": "2027-01-01T00:00:00Z", "end": "2028-01-01T00:00:00Z", }, { "operator": "VK LLC", "description": "VK 'NCA2026' Log", "url": "https://ctlog2026.mail.ru/nca2026/", "log_id": "eNkSvkIKwAIcul2iVDKRU9tIywYr/EMBJB4Qq8cWnmc=", "start": "2026-01-01T00:00:00Z", "end": "2027-01-01T00:00:00Z", }, { "operator": "VK LLC", "description": "VK 'NCA2027' Log", "url": "https://ctlog2027.mail.ru/nca2027/", "log_id": "1LN/8nNPJvHlUHXlhVtwT95xu30Fx1fT3OsTf2JDrSQ=", "start": "2027-01-01T00:00:00Z", "end": "2028-01-01T00:00:00Z", }, { "operator": "The Ministry of Digital Development and Communications", "description": "The Ministry of Digital Development and Communications " "'2026' Log", "url": "https://26.ctlog.digital.gov.ru/2026/", "log_id": "ybgRvMAcYl3KJ/TWxEfLX34Ov3LLWZGoMy+Zv5oqPko=", "start": "2026-01-01T00:00:00Z", "end": "2027-01-01T00:00:00Z", }, { "operator": "The Ministry of Digital Development and Communications", "description": "The Ministry of Digital Development and Communications " "'2027' Log", "url": "https://27.ctlog.digital.gov.ru/2027/", "log_id": "TJig3bv0dZNmDDboZiaHFAn5fnh4TpGeqWr4HFMnXNY=", "start": "2027-01-01T00:00:00Z", "end": "2028-01-01T00:00:00Z", }, ] SCT_OID = "1.3.6.1.4.1.11129.2.4.2" # ============================================================ # Helpers # ============================================================ def read_cert(path): with open(path, "rb") as f: data = f.read() if pem.detect(data): _, _, data = pem.unarmor(data) return x509.Certificate.load(data) def u16(b, off): return struct.unpack(">H", b[off:off + 2])[0] def u64(b, off): return struct.unpack(">Q", b[off:off + 8])[0] def uint24(n): return n.to_bytes(3, "big") def b64(data): return base64.b64encode(data).decode("ascii") # ============================================================ # Check remote log list for updates # ============================================================ def flatten_remote_logs(data): result = [] for operator in data.get("operators", []): operator_name = operator.get("name", "?") for log in operator.get("logs", []): interval = log.get("temporal_interval", {}) result.append({ "operator": operator_name, "description": log.get("description", "?"), "url": log.get("url"), "log_id": log.get("log_id"), "start": interval.get("start_inclusive"), "end": interval.get("end_exclusive"), }) return result def log_identity(log): return log.get("log_id") def check_log_list_update(): """ Check Yandex ctlog.json. Failure to download the list is NOT fatal. The embedded static list will still be used. """ print("Checking CT log list...") print(" URL:", REMOTE_LOG_LIST_URL) try: r = requests.get( REMOTE_LOG_LIST_URL, timeout=5, ) r.raise_for_status() data = r.json() except Exception as e: print(" Remote list unavailable:", e) print( " Using built-in list version", STATIC_LIST_VERSION ) return remote_version = str(data.get("version", "?")) remote_timestamp = data.get("log_list_timestamp", "?") print(" Built-in version:", STATIC_LIST_VERSION) print(" Remote version: ", remote_version) print(" Remote timestamp:", remote_timestamp) remote_logs = flatten_remote_logs(data) static_by_id = { log_identity(x): x for x in LOGS } remote_by_id = { log_identity(x): x for x in remote_logs } changed = False # -------------------------------------------------------- # Added logs # -------------------------------------------------------- added = ( set(remote_by_id) - set(static_by_id) ) if added: changed = True print() print(" NEW LOGS in remote list:") for log_id in sorted(added): x = remote_by_id[log_id] print() print(" +", x["description"]) print(" URL: ", x["url"]) print(" Log ID:", x["log_id"]) # -------------------------------------------------------- # Removed logs # -------------------------------------------------------- removed = ( set(static_by_id) - set(remote_by_id) ) if removed: changed = True print() print(" LOGS REMOVED from remote list:") for log_id in sorted(removed): x = static_by_id[log_id] print() print(" -", x["description"]) print(" URL: ", x["url"]) print(" Log ID:", x["log_id"]) # -------------------------------------------------------- # Changed properties for same LogID # -------------------------------------------------------- common = ( set(static_by_id) & set(remote_by_id) ) fields = [ "url", "description", "operator", "start", "end", ] for log_id in sorted(common): old = static_by_id[log_id] new = remote_by_id[log_id] differences = [] for field in fields: if old.get(field) != new.get(field): differences.append( ( field, old.get(field), new.get(field), ) ) if differences: changed = True print() print( " CHANGED:", old["description"] ) print( " Log ID:", log_id ) for field, old_value, new_value in differences: print( f" {field}:" ) print( f" built-in: {old_value}" ) print( f" remote: {new_value}" ) # -------------------------------------------------------- # Version itself changed # -------------------------------------------------------- if remote_version != STATIC_LIST_VERSION: changed = True print() if changed: print( " *** WARNING: built-in CT log list " "may be outdated ***" ) print( " The script will continue using the " "built-in list." ) else: print( " CT log list is up to date." ) # ============================================================ # SCT parsing # ============================================================ def parse_sct(sct): if len(sct) < 43: raise RuntimeError("SCT is too short") p = 0 version = sct[p] p += 1 log_id = sct[p:p + 32] p += 32 timestamp = u64(sct, p) p += 8 ext_len = u16(sct, p) p += 2 if p + ext_len > len(sct): raise RuntimeError( "Malformed SCT extensions" ) extensions = sct[p:p + ext_len] p += ext_len signature = sct[p:] return { "version": version, "log_id": log_id, "timestamp": timestamp, "extensions": extensions, "signature": signature, "raw": sct, } def get_scts(cert): extensions = cert["tbs_certificate"]["extensions"] raw = None for ext in extensions: if ext["extn_id"].dotted == SCT_OID: raw = ext["extn_value"].native break if raw is None: raise RuntimeError( "Certificate has no embedded SCT extension" ) if len(raw) < 2: raise RuntimeError("Malformed SCT list") list_len = u16(raw, 0) if list_len != len(raw) - 2: raise RuntimeError( f"Bad SCT list length: " f"declared={list_len}, " f"actual={len(raw) - 2}" ) pos = 2 result = [] while pos < len(raw): if pos + 2 > len(raw): raise RuntimeError( "Malformed SCT list" ) sct_len = u16(raw, pos) pos += 2 if pos + sct_len > len(raw): raise RuntimeError( "Malformed SCT length" ) sct = raw[pos:pos + sct_len] pos += sct_len result.append( parse_sct(sct) ) return result # ============================================================ # Match LogID -> CT log # ============================================================ def find_log_by_id(log_id_bytes): wanted = b64(log_id_bytes) for log in LOGS: if log["log_id"] == wanted: return log return None # ============================================================ # Reconstruct precertificate # ============================================================ def remove_sct_extension(cert): tbs_der = cert["tbs_certificate"].dump() tbs = x509.TbsCertificate.load(tbs_der) extensions = tbs["extensions"] found_index = None for i, ext in enumerate(extensions): if ext["extn_id"].dotted == SCT_OID: found_index = i break if found_index is None: raise RuntimeError( "SCT extension not found" ) del extensions[found_index] return tbs.dump() def issuer_key_hash(issuer): spki = ( issuer["tbs_certificate"] ["subject_public_key_info"] .dump() ) return hashlib.sha256(spki).digest() def make_precert_leaf(cert, issuer, sct): tbs = remove_sct_extension(cert) ikh = issuer_key_hash(issuer) leaf = bytearray() # Version v1 leaf += b"\x00" # MerkleLeafType = timestamped_entry leaf += b"\x00" # timestamp leaf += struct.pack( ">Q", sct["timestamp"] ) # LogEntryType = precert_entry leaf += struct.pack(">H", 1) # issuer_key_hash leaf += ikh # TBSCertificate leaf += uint24(len(tbs)) leaf += tbs # CtExtensions leaf += struct.pack( ">H", len(sct["extensions"]) ) leaf += sct["extensions"] return bytes(leaf) # ============================================================ # Merkle hashing # ============================================================ def leaf_hash(leaf): return hashlib.sha256( b"\x00" + leaf ).digest() def node_hash(left, right): return hashlib.sha256( b"\x01" + left + right ).digest() def verify_inclusion( leaf_hash_value, index, tree_size, audit_path, ): if tree_size <= 0: return None if index < 0 or index >= tree_size: return None fn = index sn = tree_size - 1 r = leaf_hash_value for p in audit_path: if fn == sn or (fn & 1): r = node_hash(p, r) while ( fn != 0 and (fn & 1) == 0 ): fn >>= 1 sn >>= 1 else: r = node_hash(r, p) fn >>= 1 sn >>= 1 if sn != 0: return None return r # ============================================================ # CT HTTP API # ============================================================ def api_url(log_url, endpoint): return ( log_url.rstrip("/") + "/ct/v1/" + endpoint ) def get_sth(log_url): r = requests.get( api_url(log_url, "get-sth"), timeout=20, ) r.raise_for_status() return r.json() def get_proof(log_url, h, tree_size): return requests.get( api_url( log_url, "get-proof-by-hash" ), params={ "hash": b64(h), "tree_size": tree_size, }, timeout=20, ) def get_entry(log_url, index): r = requests.get( api_url( log_url, "get-entries" ), params={ "start": index, "end": index, }, timeout=20, ) r.raise_for_status() data = r.json() entries = data.get( "entries", [] ) if len(entries) != 1: raise RuntimeError( f"Expected 1 entry, " f"got {len(entries)}" ) return entries[0] # ============================================================ # Check a matched SCT # ============================================================ def check_sct( log, cert, issuer, sct, ): print(" Getting STH...") sth = get_sth(log["url"]) tree_size = sth["tree_size"] advertised_root = base64.b64decode( sth["sha256_root_hash"] ) print( " tree_size:", tree_size ) leaf = make_precert_leaf( cert, issuer, sct ) h = leaf_hash(leaf) print( " Leaf hash:", b64(h) ) response = get_proof( log["url"], h, tree_size, ) print( " get-proof-by-hash HTTP:", response.status_code ) if response.status_code != 200: print(" NOT FOUND") text = response.text.strip() if text: print( " Response:", text[:500] ) return False proof = response.json() index = proof["leaf_index"] path = [ base64.b64decode(x) for x in proof["audit_path"] ] print(" FOUND") print( " leaf_index:", index ) print( " proof nodes:", len(path) ) calculated_root = verify_inclusion( h, index, tree_size, path, ) if calculated_root is None: print( " Merkle proof: INVALID" ) return False if calculated_root != advertised_root: print( " Merkle root: MISMATCH" ) print( " calculated:", calculated_root.hex() ) print( " advertised:", advertised_root.hex() ) return False print( " Merkle proof: VALID" ) # -------------------------------------------------------- # Fetch actual CT entry # -------------------------------------------------------- entry = get_entry( log["url"], index ) server_leaf = base64.b64decode( entry["leaf_input"] ) if server_leaf != leaf: print( " leaf_input comparison: DIFFERENT" ) print( " local length: ", len(leaf) ) print( " server length:", len(server_leaf) ) return False print( " leaf_input comparison: IDENTICAL" ) print() print( " *** INCLUSION FULLY VERIFIED ***" ) return True # ============================================================ # Main # ============================================================ def main(): if len(sys.argv) != 3: print( f"Usage: {sys.argv[0]} " "cert.pem issuer.pem", file=sys.stderr, ) sys.exit(2) # -------------------------------------------------------- # Check whether embedded CT log list is current # -------------------------------------------------------- check_log_list_update() print() print("=" * 72) cert = read_cert( sys.argv[1] ) issuer = read_cert( sys.argv[2] ) print() print("Reading SCTs...") scts = get_scts(cert) print( f"Found {len(scts)} " "embedded SCT(s)" ) verified = 0 for n, sct in enumerate(scts): print() print("=" * 72) print(f"SCT #{n}") print("=" * 72) log_id = b64( sct["log_id"] ) print( "Log ID:", log_id ) print( "Timestamp:", sct["timestamp"] ) # ---------------------------------------------------- # Find CT log from LogID # ---------------------------------------------------- log = find_log_by_id( sct["log_id"] ) if log is None: print() print( "CT log: UNKNOWN" ) print( "This LogID is not present " "in the built-in Yandex CT log list." ) continue print() print( "CT log:", log["description"] ) print( "Operator:", log["operator"] ) print( "URL:", log["url"] ) print( "Temporal interval:", log["start"], "..", log["end"] ) try: ok = check_sct( log, cert, issuer, sct, ) except Exception as e: print() print( "ERROR:", type(e).__name__ + ":", e ) ok = False if ok: verified += 1 # -------------------------------------------------------- # Summary # -------------------------------------------------------- print() print("=" * 72) print("FINAL RESULT") print("=" * 72) print( "Embedded SCTs:", len(scts) ) print( "Fully verified:", verified ) if verified == len(scts): print( "All SCT log entries were " "successfully verified." ) sys.exit(0) elif verified: print( "Some SCT log entries were " "successfully verified." ) sys.exit(0) else: print( "No SCT log entry was " "successfully verified." ) sys.exit(1) if __name__ == "__main__": main()
Скрипт требует некоторые зависимости:
pip install requests asn1crypto
Для работы ему нужен сам сертификат и выпускающий сертификат CA (подойдут файлы промежуточных CA из архива — https://gu-st.ru/content/lending/russian_trusted_sub_ca.zip).
Как использовать:
# ct.py - сам скрипт (для Python 3) # cert.pem - сертификат нужного домена в формате PEM (файлы .crt внутри часто тоже идут с PEM) # subca.pem - сертификат промежуточного CA python ct.py cert.pem subca.pem
Пример работы:
python ct.py _.online.sberbank.ru.crt subca.pem


Ложка дёгтя
Или скорее ведро помоев.
С сертификатами ГОСТ все это на практике не работает.
Во-первых, почему-то в сертификатах ГОСТ часто (всегда?) не содержатся записи SCT (это пометки о включении в CT-логи, по которым потом и происходит проверка). Так, например, злосчастный сертификат сайта Госуслуг из прошлой статьи их не имеет:
Reading SCTs... Traceback (most recent call last): File "/***/ct.py", line 998, in <module> main() File "/***/ct.py", line 852, in main scts = get_scts(cert) File "/***/ct.py", line 381, in get_scts raise RuntimeError( RuntimeError: Certificate has no embedded SCT extension
Я пока не нашел сайта, который был поддерживал подключение по ГОСТ и имел сертификат, подписанный актуальными корневыми Минцифры. Поэтому проверить поведение Яндекса в ситуации отсутствия SCT/CT-логов не могу. Если кто-то найдет пример, поделитесь.
(тут было "во-вторых", но я лопухнулся... оставлю для истории).
Надеюсь, было небесполезно.
Комментарии (29)

pnetmon
23.08.2026 15:19Я пока не нашел сайта, который был поддерживал подключение по ГОСТ и имел сертификат, подписанный актуальными корневыми Минцифры
думается не там ищите, т.к. подключение по ГОСТ требует помимо браузера поддерживающего ГОСТ еще и установку "платного" криптопровайдера и сейчас это будут какие-то л.к., а не популярные сайты. Год назад приходилось резко внедрять в рабочей среде браузер с поддержкой ГОСТ, КриптоПро, сертификаты для входа в личные кабинеты с сертификатом выпущенным именно Минцифрой, для себя тогда нашлось какое-то самоуспокоение установки КриптоПро без его покупки из-за фразы
https://cryptopro.ru/products/chromium-gost
В качестве криптопровайдера мы рекомендуем использовать КриптоПро CSP. Для работы с защищёнными соединениями не требуется покупка лицензии КриптоПро CSP

galaxy Автор
23.08.2026 15:19Я полазил по некоторым л.к. Там все плохо с цепочками доверия: в корне что попало будет. Хотелось найти что-то, что использует официальные корневые сертификаты...

FotoHunter
23.08.2026 15:19Я активно работаю с этими сертами. Можно поставить и на nginx и его форк, НО задолбаешся с OPENSSL, одновременно openssl поддерживающий ГОСТ и нормальный работают только в КриптоПро, собрать "всеядную" систему крайне сложно. А на гос.сайты сейчас пришло требование наличия 2х параллельных сертов RSA + ГОСТ оба из Минцифры. В чистом яндекс откроется только RSA с цепочка и доверия там всё хорошо, если админ сайта фулчейн правильно собрал(сайт + sub + CA). А вот если стоит КриптоПро + плагин для браузера, тогда на том же сайте можно увидеть ГОСТ (и алгоритм "Кузнечик") и с цепочка и доверия тоже всё хорошо + проверка на отзыв серта.

alexzen
23.08.2026 15:19Все эти решения с проверкой прозрачности, переподписованием - жутко неудобно. Не каждый технарь захочет в этом всем разбираться, а про обычных людей я, вообще, молчу. Нужно решение, которое одной кнопкой будет ограничивать зоны действия сертификатов. Ждем более простых решений. А пока их нет, второй браузер с локальными сертификатами частично решает проблему.

saipr
23.08.2026 15:19Я пока не нашел сайта, который был поддерживал подключение по ГОСТ и имел сертификат, подписанный актуальными корневыми Минцифры.
Тогда как можно говорить
О доверии к отечественным сертификатам
???

galaxy Автор
23.08.2026 15:19Большинство отечественных сертификатов - не ГОСТовские.

saipr
23.08.2026 15:19Т. е. твердя об импортозамещении в IT, в святая святых в криптографии продолжает использоваться западная криптография. Ну этим все сказано.

yahooyaks
23.08.2026 15:19Еще руки не дошли, еще не все тендеры закрыты. Вот как проголодаются, так за перепиливание криптографии возьмутся.

Underkom
23.08.2026 15:19Юзаешь западную ОС - получаешь западную криптографию ) А для повсеместной ГОСТ крипты в .ru зоне, нужно что бы все по умолчанию юзали АРМ, серверные и мобильные рашн ОС, где криптопровайдер будет встроен и бесплатен... Ну а пока юзаем rsa)

sigprof
23.08.2026 15:19Я пока не нашел сайта, который был поддерживал подключение по ГОСТ и имел сертификат, подписанный актуальными корневыми Минцифры.
В Telegram-канале RussianTLS, ссылка на который есть на https://www.gosuslugi.ru/landing/tls, пишут, что пока для сертификатов ГОСТ от Минцифры доступны не все способы выпуска, и SCT в них не добавляются:
> планируется ACME расширить на работу с ГОСТом?
Не в ближайшее время точно. У нас выпуск гост через воздушный зазор.Для ГОСТ из-за наличия воздушного зазора пре-сертификаты в CTLOG не публикуются, а сами сертификаты не содержат SCT-метки.

galaxy Автор
23.08.2026 15:19Простите, конечно, деревенского, но что такое "воздушный зазор"?

Sap_ru
23.08.2026 15:19Собирают запросы на выпуск сертификатов, пишут на флэшку (кроме шуток), ногами несут флшку в охряняемую дядькой с пистолетом с комнату без интернета, суют в компьютер с корневым ключом, подписывают, несут флэшку обратно и рассылают результат запросившим.
Во-первых, древность дикая. И поэтому выпуск занимает несколько дней.
Во-вторых, вызывает вопросы по безопасности, так как присутсвует физическая флэшка с запросами и выпущенными сертификатами, которую можно украсть/скопировать, тем более, что её физически перемещают. А если, например не очистят после? А её точно чистят? Короче офигенно безопасно с точки хрения организаторов, но очень плохо с точки зрения пользователей.
В-третьих, ничего не мешает к этому прикрутить CTLOG. Просто записи о запросе и выпуске будут разнесены по времени. Так что это всё отмазки и кривые руки.
ArtyomOchkin
23.08.2026 15:19Мда. Если они действительно через такой древний способ (хорошо, хоть не на дискетах носят) добавляют эти сертификаты, то имхо на основном домашнем компьютере в принципе лучше не иметь ПО с отечественными сертификатами (без изоляции)...

Yuriy_krd
23.08.2026 15:19дискеты были до 2018 года, примерно (я носил) ) Сей час - флешки) многие МУП, ФГУП и прочие госструктуры, до сих пор, носят запрос на серт (для работы на тендерных площадках, например) на флешке в региональные казначейства, где им записывают серты и ключи на флешки.

mvv-rus
23.08.2026 15:19Собирают запросы на выпуск сертификатов, пишут на флэшку (кроме шуток), ногами несут флшку в охряняемую дядькой с пистолетом с комнату без интернета, суют в компьютер с корневым ключом, подписывают, несут флэшку обратно и рассылают результат запросившим. Во-первых, древность дикая. И поэтому выпуск занимает несколько дней.
У вас хорошее воображние, но… Во-первых, корневым ключом подписываются не конечные сертификаты, а сертификаты промежуточных выпускающих центров сертификации (ЦС), которые уже, в свтю очередь подписывают конечные сертификаты. И если вы посмотрите, что предлагает скачать Минцифры, то увидите, что таковой у них есть (и в принципе, сертифкат промежуточного ЦС при правильной организации PKI можно даже не качать: место, откуда его можно скачать прописяывается в расширении AIA всех выпущенных им сертификатов). Таких сертификатов промежуточных ЦС немного, перевыпускаются они редко, риск утечки их секретного ключа вполне управляем: достаточно в случае утечки добавить утекший сертификат промежуточного ЦС в CRL… Поэтому сомнительно, что нафантазированная вами процедура используется для выпуска конечных сертификатов. И задержка с выпуском, скорее всего, связан с необходимостью проверки того, что запросивший - действительно тот, за кого он себя выдает. И ЕМНИП у традиционных ЦС (не LetsEncrypt - Verisign и пр.) запрос в 00-е тоже обрабатывался довльно замаетное время.
А вот для выпуска сертификатов промежуточных выпускающих ЦС, которые подписывать должен корневой ЦС, такая процедура вполне оправдана - потому что в случае утечки секретного ключа корневого сертификата последствия устранить куда труднее: для этого его нужно будет удалить на всех оконечных устройствах.
Во-вторых, вызывает вопросы по безопасности, так как присутсвует физическая флэшка с запросами и выпущенными сертификатами, которую можно украсть/скопировать, тем более, что её физически перемещают. А если, например не очистят после? А её точно чистят? Короче офигенно безопасно с точки хрения организаторов, но очень плохо с точки зрения пользователей.
Вопросов не возникает - потому что на этой “флэшке” ничего секретного быть не должно. Запросы на сертификат вы можете сгенерировать себе хоть сами (в Windows для этого используется certreq.exe), а выпущенные конечные сертификаты часто доступны публично (в частности, для работы TLS они обязательно доступны: свой сертификат сервер в процессе согласования соединения шлёт). Секретных ключей конечных сущностей там, при правильной организации PKI, быть не должно - для подписывания они чисто технически совершенно не требуются.
В-третьих, ничего не мешает к этому прикрутить CTLOG. Просто записи о запросе и выпуске будут разнесены по времени.
А вот CTL прикрутить технически действительно, наверное, можно: хотя там свои криптопротоколы, и хотя GOST среди них нет (по крайней мере в RFC 9162 я его не видел), но, AFAIK, с протоколом, используемым для подписи сертифката протокол для CTL не связан. Что этому мешает организационно - не знаю и не интересуюсь: пусть Яндекс сам над сценариями использования своего браузера думает, в моих сценариях использования я проверкой по CTL не пользуюсь, как излишней.

Pavel7
23.08.2026 15:19вызывает вопросы по безопасности, так как присутсвует физическая флэшка с запросами и выпущенными сертификатами, которую можно украсть/скопировать, тем более, что её физически перемещают
Присоединяюсь к комментатору выше, в чём вопросы безопасности с CSR-ами и выпущенными сертификатами? И где почитать, что запросы на выпуск конечных сертификатов ногами носят на флэшке?

redfox0
23.08.2026 15:19Я пока не нашел сайта, который был поддерживал подключение по ГОСТ и имел сертификат, подписанный актуальными корневыми Минцифры.

xemos
23.08.2026 15:19По такой логике вообще любой сертификат в хранилище корневых сертификатов подвергают нас риску MiTM.

vesper-bot
23.08.2026 15:19формально да, подвергают, для борьбы с этим риском придумали CTlog, который в статье пытаются проверять. MITMный серт если попадет в такой лог - это ЗАШКВАР на весь мир сразу, потому что лог обязан быть публичным, а если не попадет, то браузер ему доверия не построит, потому что не прописан в публичных логах.
alexs963
Ограничиваем сертификат Минцифры
Kenya-West
Кстати, а никто из энтузиастов таким образом не родил поддерживаемый сообществом самоподписанный кросс-сертификат? Желательно с разными вариантами подмножеств: черный список, только ru-su-рф, только сайты и их ресурсы перешедшие на root CA Минцифры. Удобно же - тот же Минцифры, только принудительно ограниченный нужной зоной.
mvv-rus
А какие основания доверять такому сертификату, выпущенному каким-то совершенно левым автором, не задумывались? Может, поэтому энтузиасты свой энутузиазм придерживают, а сообщество тех, кому-таки оказалось невтерпёж, не поддерживает ?
Kenya-West
Ха-ха, самое смешное, что оснований доверять сертификату МинЦифры тоже не больше, чем вот такому самоподписному, от котором я выше писал.
Кто захочет, тот поставит, кто не захочет, пройдёт мимо.
Не всем ведь подряд вкатываться в криптографию и умело играть с созданием приватно-публичных ключей, попутно разбираясь с тонкостью настроек всех этих TLS. Я лично понятия не имею, как это делать... А защититься от MitM на ТСПУ хочется.