В комментариях к моей недавней статье про фейл с ГОСТ сертификатом сайта ГосУслуг возникла дискуссия о доверии к российским корневым сертификатам Минцифры и о рисках их установки (в систему, в отдельный браузер, или риск использования Яндекс браузера, где они встроены).

Здесь вдаваться в подробности и повторять сказанное много раз не буду, включу только некое саммари из основных тезисов для тех, кто не погружен в тему:

  1. Сами корневые сертификаты условно безопасны

    Извините, этот пункт для домохозяек — сами сертификаты ваш компьютер не взломают. Установка их в доверенное хранилище открывает путь к потенциальным атакам (см. ниже).

  2. Если вы их поместили в доверенное хранилище, потенциально ваш трафик определенные лица могут расшифровать и даже подделать

    Пояснения: лица, имеющие доступ к трафику (например, через печально известные ТСПУ), могут с помощью сертификата промежуточного УЦ (подписанного корневым) выпустить в онлайн режиме поддельный сертификат для сайта, с которым вы намереваетесь связаться — допустим, gmail.com или telegram.org. Это называется MitM-атака, на промежуточном сетевом узле трафик можно читать или изменять по желанию атакующего.

    Хоть и не хочу ссылаться на этого нейрослопового школотрона (zarazaexe), но свежее и доходчивее ничего не нашел — почитайте.

  3. Защититься от такой атаки сложно

    Можно попытаться ограничить потенциальный ущерб. Скажем, установить корневые отечественные сертификаты в отдельный браузер или профиль и использовать его только, чтобы заходить на российские сайты, на которые иначе зайти нельзя.

    Также с помощью диких костылей (уж простите, @outlingo) можно ограничить зону действия корневых сертификатов (скажем, до *.ru).

  4. 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
В логе VK запись есть
В логе VK запись есть
2 из 3-х логов подтвердили (к логу Минцифры скрипт законнектиться не смог)
2 из 3-х логов подтвердили (к логу Минцифры скрипт законнектиться не смог)

Ложка дёгтя

Или скорее ведро помоев.

С сертификатами ГОСТ все это на практике не работает.

Во-первых, почему-то в сертификатах ГОСТ часто (всегда?) не содержатся записи 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)


  1. alexs963
    23.08.2026 15:19

    1. Kenya-West
      23.08.2026 15:19

      Кстати, а никто из энтузиастов таким образом не родил поддерживаемый сообществом самоподписанный кросс-сертификат? Желательно с разными вариантами подмножеств: черный список, только ru-su-рф, только сайты и их ресурсы перешедшие на root CA Минцифры. Удобно же - тот же Минцифры, только принудительно ограниченный нужной зоной.


      1. mvv-rus
        23.08.2026 15:19

        Кстати, а никто из энтузиастов таким образом не родил поддерживаемый сообществом самоподписанный кросс-сертификат?

        А какие основания доверять такому сертификату, выпущенному каким-то совершенно левым автором, не задумывались? Может, поэтому энтузиасты свой энутузиазм придерживают, а сообщество тех, кому-таки оказалось невтерпёж, не поддерживает ?


        1. Kenya-West
          23.08.2026 15:19

          Ха-ха, самое смешное, что оснований доверять сертификату МинЦифры тоже не больше, чем вот такому самоподписному, от котором я выше писал.

          Кто захочет, тот поставит, кто не захочет, пройдёт мимо.

          Не всем ведь подряд вкатываться в криптографию и умело играть с созданием приватно-публичных ключей, попутно разбираясь с тонкостью настроек всех этих TLS. Я лично понятия не имею, как это делать... А защититься от MitM на ТСПУ хочется.


  1. pnetmon
    23.08.2026 15:19

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

    думается не там ищите, т.к. подключение по ГОСТ требует помимо браузера поддерживающего ГОСТ еще и установку "платного" криптопровайдера и сейчас это будут какие-то л.к., а не популярные сайты. Год назад приходилось резко внедрять в рабочей среде браузер с поддержкой ГОСТ, КриптоПро, сертификаты для входа в личные кабинеты с сертификатом выпущенным именно Минцифрой, для себя тогда нашлось какое-то самоуспокоение установки КриптоПро без его покупки из-за фразы

    https://cryptopro.ru/products/chromium-gost

    В качестве криптопровайдера мы рекомендуем использовать КриптоПро CSP. Для работы с защищёнными соединениями не требуется покупка лицензии КриптоПро CSP


    1. galaxy Автор
      23.08.2026 15:19

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


    1. redfox0
      23.08.2026 15:19

      del


    1. FotoHunter
      23.08.2026 15:19

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


    1. eri
      23.08.2026 15:19

      Яндекс браузер для организаций ещё поддерживает гост


  1. alexzen
    23.08.2026 15:19

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


  1. saipr
    23.08.2026 15:19

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

    Тогда как можно говорить

    О доверии к отечественным сертификатам

    ???


    1. galaxy Автор
      23.08.2026 15:19

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


      1. saipr
        23.08.2026 15:19

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


        1. yahooyaks
          23.08.2026 15:19

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


        1. Underkom
          23.08.2026 15:19

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


  1. sigprof
    23.08.2026 15:19

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

    В Telegram-канале RussianTLS, ссылка на который есть на https://www.gosuslugi.ru/landing/tls, пишут, что пока для сертификатов ГОСТ от Минцифры доступны не все способы выпуска, и SCT в них не добавляются:

    > планируется ACME расширить на работу с ГОСТом?
    Не в ближайшее время точно. У нас выпуск гост через воздушный зазор.

    Для ГОСТ из-за наличия воздушного зазора пре-сертификаты в CTLOG не публикуются, а сами сертификаты не содержат SCT-метки.


    1. galaxy Автор
      23.08.2026 15:19

      Простите, конечно, деревенского, но что такое "воздушный зазор"?


      1. lryzhik
        23.08.2026 15:19

        air gap. нет подключения к защищенной сети извне. перенос информации, как правило, через съемный носитель (флешка?)


        1. mvv-rus
          23.08.2026 15:19

          Запрос к ЦС, в принципе, можно переносить хоть на листочке: это - текстовый файл, который можно распечатать и набить потом вручную.


          1. blind_oracle
            23.08.2026 15:19

            На перфокартах!


      1. Sap_ru
        23.08.2026 15:19

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


        1. ArtyomOchkin
          23.08.2026 15:19

          Мда. Если они действительно через такой древний способ (хорошо, хоть не на дискетах носят) добавляют эти сертификаты, то имхо на основном домашнем компьютере в принципе лучше не иметь ПО с отечественными сертификатами (без изоляции)...


          1. Yuriy_krd
            23.08.2026 15:19

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


        1. 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 не пользуюсь, как излишней.


        1. Pavel7
          23.08.2026 15:19

          вызывает вопросы по безопасности, так как присутсвует физическая флэшка с запросами и выпущенными сертификатами, которую можно украсть/скопировать, тем более, что её физически перемещают

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


  1. redfox0
    23.08.2026 15:19

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

    https://priem-online.ru/all


  1. xemos
    23.08.2026 15:19

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


    1. Funbit
      23.08.2026 15:19

      Ну без CT логов по сути так оно и есть.


    1. vesper-bot
      23.08.2026 15:19

      формально да, подвергают, для борьбы с этим риском придумали CTlog, который в статье пытаются проверять. MITMный серт если попадет в такой лог - это ЗАШКВАР на весь мир сразу, потому что лог обязан быть публичным, а если не попадет, то браузер ему доверия не построит, потому что не прописан в публичных логах.