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

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

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

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

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

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

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

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

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

    Можно, тем не менее, ограничить зону действия корневых сертификатов — см. ниже.

  4. Certificate Transparency логи и Яндекс браузер — неплохое решение в плане безопасности

    Про Certificate Transparency можно почитать тут (ну или в википедии). Вкратце: запись о выданном сертификате включается в типа‑блокчейн, а в сам серт — пометки об этом.

    У меня мало хороших слов в адрес Яндекс браузера и Яндекса вообще, но тут им надо отдать должное: они инициировали и продавили решение все выпускаемые отечественные сертификаты записывать в CT‑логи (ссылка).

    Яндекс браузер (если не врёт) без записи в CT‑логе сертификат не примет. Это очень осложняет онлайн MitM‑атаки и оставляет следы от попыток.

    Другие браузеры при добавлении внешних корневых сертификатов проверку CT‑логов для соответствующих цепочек отключают (ну типа думают, что они в корпоративной среде).

Ограничение корневых сертификатов

Область действия корневых сертификатов можно сузить до определенной DNS зоны или даже списка доменов.

Способ № 1

Почитать можно тут (LOR) или тут (Хабр).

Суть: создаем свой самоподписанный CA сертификат с раширением nameConstraints (в нем список нужных доменных зон), создаем клон корневого сертификата Минцифры с теми же DN, расширениями и Public key, подписываем клон с помощью своего CA все кладем в доверенное хранилище.

Есть утилита от хабраюзера @umatrix — https://github.com/umatrix81/certsru

Плюсы: работает на всю систему (любые приложения смогут использовать корневые сертификаты).

Минусы: работает на всю систему ?, некоторая костыльность.

Способ № 2

Для хромоподобных браузеров (кроме Яндекса) есть политика CACertificatesWithConstraints. Задает список корневых сертификатов и перечень доменов для каждого (на поддомены распространяется автоматически).

В Windows кладется в реестр (или в групповые политики):

  • Google Chrome: Software\Policies\Google\Chrome\CACertificatesWithConstraints

  • MS Edge: SOFTWARE\Policies\Microsoft\Edge

Подробнее тут (+ простой скрипт для управления).


Цель этой статьи — кратко рассказать об 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‑логами

То, что Яндекс проверяет наличие записи в логах, конечно, хорошо... А вот как бы самому руками все это пощупать?

Сами логи устроены максимально человеконенавистнически, и просто проверить по ссылочке, все ли хорошо у сертификата с включением в лог, нельзя.

Вообще, добрые люди придумали сервисы мониторинга (наподобие визуализаторов блокчейна), которые парсят эти логи и позволяют поискать и покликать. Но глобальные сервисы с российскими логами не работают (а самый известный crt.sh — так по‑моему вообще сдох). Есть отечественный аналог — ct.tlscc.ru. И снова «но» — похоже, что он тоже слегка заброшен: так, некоторых сертификатов (например, актуальный для db.vtb.ru) я там не нашел, и вообще, кажется, что он не работал с новыми адресами CT‑логов 2027 года (см. выше, например: https://ct‑agate.yandex.net/2027/).

Update: нашелся приличный поисковик по CT‑логам — precert.ru. Вот, например, запись для онлайн‑банка Сбера — precert.ru/19253/.

Так что я, смирив стыд, помучил 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 из архива).

Как использовать:

# 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‑логов не могу. Если кто‑то найдет пример, поделитесь.

(тут было «во‑вторых», но я лопухнулся... оставлю для истории).

Надеюсь, было небесполезно.

Комментарии (58)


  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. mvv-rus
            23.08.2026 15:19

            Доверие - штука субъективная. Вы, вот, почему-то не доверяете совсем, многие другие - доверяют (и MitM им пофиг), а я, подобно автору недавней статьи, доверяю ограниченно: для доменных имен сайтов в зоне .ru - доверяю (те, кто контролируют ТСПУ, могут получить с этих сайтов любую информацию безо всякого MitM), в остальном - нет.


      1. usrsse2
        23.08.2026 15:19

        ru, su, рф недостаточно — есть ещё, например, .moscow и .москва


  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. bb0ff
        23.08.2026 15:19

        Извините, конечно, но рассказы про стрррашные сложности с openssl мне напоминают такие же триллеры про собственную почту, которая никогда никуда не придёт без аЦЦкого неизвестно чего. Почему-то у меня ни малейших проблем с двумя сертами (ГОСТ и RSA) в nginx или angie не возникает, как и с собственным почтовиком. Делается за 10 минут с перекуром и просто работает.
        Вот, например, докерфайл для nginx с ГОСТ и RSA:

        FROM registry.astralinux.ru/library/astra/ubi17-nginx1280:1.7.9
        
        ENV TZ="Europe/Moscow"
        ENV PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
        
        RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
            echo $TZ > /etc/timezone && \
            apt-get -o Acquire::GzipIndexes=false update && \
            apt-get install --no-install-recommends --no-install-suggests -y libgost-astra && \
            ln -sf /dev/stdout /var/log/nginx/access.log && \
            ln -sf /dev/stderr /var/log/nginx/error.log && \
            mkdir -p /etc/nginx/ &&\
            apt-get remove --purge --auto-remove -y && \
            rm -rf /var/lib/apt/lists/*
        
        EXPOSE 80 443
        
        STOPSIGNAL SIGQUIT
        
        CMD ["nginx", "-g", "daemon off;"]
        

        А вот к нему README.md:

        ### nginx-gost-astra
        По сути взят официальный базовый образ Astra+nginx и выполнено `apt-get install libgost-astra`.
        Пример конфигурации nginx для работы с ГОСТ и RSA параллельно:
        ```
        server {
            listen 80;
            server_name santa.nn;
            return 301 https://$server_name$request_uri;
        }
        server {
            listen 443 ssl;
            server_name santa.nn;
            ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
            ssl_certificate        /etc/nginx/ssl/santa_gost.crt;
            ssl_certificate_key    /etc/nginx/ssl/santa_gost.key;
            ssl_certificate        /etc/nginx/ssl/santa.crt;
            ssl_certificate_key    /etc/nginx/ssl/santa.key;
            ssl_prefer_server_ciphers on;
            ssl_ciphers GOST2012-GOST8912-GOST8912:GOST2001-GOST89-GOST89:ECDH+AESGCM:ECDH+AES256:ECDH+AES128:ECDH+3DES:RSA+AES:RSA+3DES:!ADH:!AECDH:!MD5:!DSS;
        }
        ```
        


        Пользуйтесь, если кому надо. Да, на базе astra'ы, но это требования к тем самым сайтам, на которых такой гибрит бывает нужен.


        1. FotoHunter
          23.08.2026 15:19

          Серёжа, этот контэйнер кто то (служивые из Астры) собрал, а тебе повезло его найти. Я в прошлом году искал и не нашёл подобных решений.


          1. bb0ff
            23.08.2026 15:19

            Саша, я тот же фокус делал на убунте 24 и 26, вся разница — там пакет для gost-engine не прописывается в конфиг openssl, его надо добавить руками. И всё!


    1. eri
      23.08.2026 15:19

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


    1. Shaman_RSHU
      23.08.2026 15:19

      ViPNet CSP бесплатный. Такое же сертифицированное СКЗИ, как и КриптоПро CSP и поддерживает те же алгоритмы ГОСТового шифрования.


  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. freeExec
            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. freeExec
              23.08.2026 15:19

              Получил флешку с ключами - оставь сразу заявление на компрометацию.


        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. galaxy Автор
      23.08.2026 15:19

      Корневой тоже от 2022 года :( Та же проблема, что и в статье про госуслуги.


  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ный серт если попадет в такой лог - это ЗАШКВАР на весь мир сразу, потому что лог обязан быть публичным, а если не попадет, то браузер ему доверия не построит, потому что не прописан в публичных логах.


      1. Aelliari
        23.08.2026 15:19

        а если не попадет, то браузер ему доверия не построит, потому что не прописан в публичных логах.

        Это касается CA из поставки браузера или системного хранилища. Для пользовательских CA они не проверяются


      1. freeExec
        23.08.2026 15:19

        MITMный серт если попадет в такой лог

        Так а как из лога узнать, что он не настоящий и его выпуском занимался товарищ майор?


        1. Ndochp
          23.08.2026 15:19

          Никак, предполагается что за логом следит легитимный выпускатор и на все левые записи сразу делает запись на отзыв. Особенно если вдруг сертификатом минцифры начинает подписываться сайт github.com


  1. mixsture
    23.08.2026 15:19

    Ну собственно их три: Yandex Agate Log, VK NCA Log, The Ministry of Digital Development and Communications Log

    Т.е. журнал ведут 3 госкомпании и ни одного независимого участника. Как теперь поверить, что с журналами не проводят махинаций?


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

      Смотря, каких махинаций. Лог похож на блокчейн, удалить или изменить что-то там нельзя (без перестройки всего лога).

      Ну т.е. как бы в принципе, если всем вообще пофиг на эти логи, и никто их никогда не мониторит, то держатель лога может чудить по своему усмотрению.


      1. mixsture
        23.08.2026 15:19

        Именно. Для достоверности нам нужны наблюдатели с копией всего лога и частым сравнением. А их нет. Без них блокчейно-подобный журнал - лишь обычный журнал с очень cpu-intensive изменениями в старых записях.


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

          Для достоверности нам нужны наблюдатели с копией всего лога и частым сравнением

          Нет, и копий не надо, и частых сравнений. Достаточно архивировать периодически Signed Tree Head.


          1. mixsture
            23.08.2026 15:19

            Мне думается, что в таком случае можно будет определить только факт измененности. А для определения “что изменили” (в частности, с какой записи начали переписывать) нужно все же хранить весь лог.


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

              Само собой. Но в адекватной вселенной после такого переписывания лога он идет в мусорку вместе с издателем.


              1. ifap
                23.08.2026 15:19

                Это в адекватной. Давайте вообразим, что сегодня мы получили пруф подделки CT-лога и...? Хабр негодует, гики массово сносят "сувенирные" серты даже из своих контейнеров, отдельных профилей браузеров и прочих огороженных пространств. На этом всем, ширнармассы продолжают жрать кактус, даже не особо всхлипывая, так как по зомбоящику им не раскажут доходчиво об инциденте и его значении.


                1. mvv-rus
                  23.08.2026 15:19

                  А этот инцидент имеет значение? По моему, CTL - это пятое колесо в телеге PKI (потому что владелец корневого ЦС уже отвечает за всё, что подписал и он, и другие ЦС, сртификаты которых им подписаны), и непонятно почему создатели Яндекс-браузера придают ему какое-то значение. Разве что, используют как костыль?


      1. mvv-rus
        23.08.2026 15:19

        Лог похож на блокчейн, удалить или изменить что-то там нельзя (без перестройки всего лога).

        Но всегда можно что-то добавить - никакой блокчейн от этого сам по себе не защищен.И если все три госкомпании подчиняются одному центру принятия решений – что, на самом деле, не факт, потому как владельцы разных CTL могут подчиняться разным “бульдогам под ковром”, более известным как “башни”, – то они дружно скажут “так и было”. Но вот противоречия между разными башнями так вылезут наружу, да.


        1. Ndochp
          23.08.2026 15:19

          Так мы МИТМ на VK не боимся, они и без митма все данные сдадут. А вот если в лог минцифры добавится сертификат на google.com (чтобы узнать, что вы там ищете) - у всех появятся вопросы.


          1. mvv-rus
            23.08.2026 15:19

            Дык, если такой сертификат - в цепочке сертификатов с корнем от Минцифры - кто-то выпустит и начнет использовать - это почти наверняка будет замечено и на том же Хабре редакция обязательно опубликкет про это новость. А заметить такое несложно - я подобное замечал, помню.
            Лично я сохранил в памяти эпизод, когда именно я глядел на чей-то (уже точно не помню) сертифкат от Let’s Encrypt в точности как те пограничники в Интербеллуме на польский паспорт, что описано Маяковским в стихе о советском паспорте - типа, откуда это и чо это за (ну, я про этот ЦС узнал далеко не сразу).

            Ну, а как принудительно ограничить область действия сертификатов (Минцифры или кого ещё) - про это свосем недавно была статья на Хабре. Так что пользц от CTL я не вижу, равно как и повышенного основания доверять его владельцу по сравнению с владельцем корневого ЦС.


  1. mixsture
    23.08.2026 15:19

    Вообще, я бы пошел другим путем. В браузеры бы добавил сущность “контейнер для сертификатов”, а у него уже могут быть правила активации. Если правила активации не выполнены, то контейнера как бы не существует и всех сертификатов в нем тоже. В правила можно задавать ip-based, region-based, name-based - ограничения и разрешения. Контейнерами можно не пользоваться - т.е. просто пользоваться каким-то контейнером по-умолчанию “all”, который активен всегда.

    Тогда мы не пытаемся как-то видоизменить под себя инфраструктуру криптографии (ибо весь мир не захочет ее менять, тут у нас локальные проблемы на несколько процентов населения планеты). Но при этом получаем контролируемую изоляцию “подозрительных” сертификатов.


    1. ddr6
      23.08.2026 15:19

      В хроме это уже есть и даже статья (https://habr.com/ru/articles/1066372/) на хабре была месяц назад. Я даже навайбкодил для себея простое gui приложение для контроля списков сайтов, ради которых chrome вспоминает, что есть серт минцифры. Мне очень странно, что все упорно продолжают игнорировать этот способ.


  1. umatrix
    23.08.2026 15:19

    Навайбокдил утилиту для генерации самоподписанного сертификата и powershell скрипта для установки в винде - https://github.com/umatrix81/certsru


  1. georgiy08
    23.08.2026 15:19

    (а самый известный https://crt.sh/ — так по‑моему вообще сдох)

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


  1. ddr6
    23.08.2026 15:19

    Для хрома все уже придумано и даже не требует установки серта минцифры куда-либо.

    https://habr.com/ru/articles/1066372/


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

      Добавил в статью