Как в Go проверить валидность TLS/x509 сертификата?

Ответ

Для проверки валидности сертификата в Go используется пакет crypto/x509 и метод Certificate.Verify().

Основная идея — проверить, что цепочка сертификатов от сервера ведет к одному из доверенных корневых сертификатов (root CA), которые хранятся в системе или предоставлены вручную.

package main

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "log"
)

func main() {
    // Устанавливаем TLS-соединение с сервером
    conn, err := tls.Dial("tcp", "google.com:443", &tls.Config{
        // InsecureSkipVerify = true нужен, чтобы получить сертификат,
        // даже если он не проходит автоматическую проверку.
        // Мы проведем проверку вручную.
        InsecureSkipVerify: true,
    })
    if err != nil {
        log.Fatalf("Не удалось подключиться: %v", err)
    }
    defer conn.Close()

    // Получаем цепочку сертификатов от сервера
    certs := conn.ConnectionState().PeerCertificates
    if len(certs) == 0 {
        log.Fatal("Сервер не предоставил сертификат")
    }

    // Загружаем системный пул корневых сертификатов
    rootCAs, err := x509.SystemCertPool()
    if err != nil {
        log.Fatalf("Не удалось загрузить системные сертификаты: %v", err)
    }

    // Создаем опции для проверки. Мы доверяем системным корневым сертификатам.
    opts := x509.VerifyOptions{
        Roots: rootCAs,
        // Можно указать DNS-имя для дополнительной проверки
        DNSName: "google.com",
    }

    // Проверяем сертификат сервера (первый в цепочке)
    // Метод Verify сам построит и проверит всю цепочку до корневого.
    _, err = certs[0].Verify(opts)
    if err != nil {
        fmt.Printf("Сертификат недействителен: %vn", err)
    } else {
        fmt.Println("Сертификат действителен!")
    }
}

Что именно проверяет cert.Verify(opts)?

  • Цепочку доверия: Проверяет, что каждый сертификат в цепочке подписан предыдущим, и вся цепочка заканчивается доверенным корневым сертификатом из opts.Roots.
  • Срок действия: Убеждается, что текущее время находится в промежутке между NotBefore и NotAfter для каждого сертификата в цепочке.
  • Имя хоста (DNS Name): Если указано в opts.DNSName, проверяет, что сертификат выдан для этого домена.
  • Назначение ключа (Key Usage): Проверяет, что сертификат предназначен для тех целей, для которых он используется (например, для аутентификации сервера).

Ответ 18+ 🔞

Да ты посмотри, какая хуйня творится в мире TLS! Все эти сертификаты, цепочки доверия... Прям как в жизни, блядь — чтобы кому-то поверить, нужно знать, кто его породил, и чтоб этот предок не был каким-нибудь пидарасом шерстяным.

Вот смотри, в Go за всю эту ебучую проверку отвечает пакет crypto/x509. Главный там метод — Certificate.Verify(). Суть проста, как три рубля: нужно убедиться, что бумажка от сервера тянется цепочкой до какого-нибудь уважаемого корневого деда (root CA), которого мы уже знаем и которому доверяем, как себе.

Вот, держи пример, как это выглядит на практике:

package main

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "log"
)

func main() {
    // Цепляемся к серверу по TLS
    conn, err := tls.Dial("tcp", "google.com:443", &tls.Config{
        // InsecureSkipVerify = true — это типа "отстань, я сам всё проверю".
        // Без этого нам даже сертификат не покажут, если что не так.
        InsecureSkipVerify: true,
    })
    if err != nil {
        log.Fatalf("Не удалось подключиться: %v", err)
    }
    defer conn.Close()

    // Вытаскиваем целую пачку сертификатов, которые сервер нам сунул
    certs := conn.ConnectionState().PeerCertificates
    if len(certs) == 0 {
        log.Fatal("Сервер не предоставил сертификат")
    }

    // Тащим системный набор корневых уважаемых дедов
    rootCAs, err := x509.SystemCertPool()
    if err != nil {
        log.Fatalf("Не удалось загрузить системные сертификаты: %v", err)
    }

    // Готовим правила для допроса
    opts := x509.VerifyOptions{
        Roots: rootCAs, // Кому мы верим
        DNSName: "google.com", // А на бланке должно быть это имя, а не левое
    }

    // Ну и начинаем проверять самый первый сертификат (тот, что от сервера)
    // Verify сам пройдется по всей цепочке предков до корня.
    _, err = certs[0].Verify(opts)
    if err != nil {
        fmt.Printf("Сертификат недействителен: %vn", err)
    } else {
        fmt.Println("Сертификат действителен!")
    }
}

А что конкретно этот cert.Verify(opts) выёбывается и проверяет?

  • Цепочку, блядь, доверия: Смотрит, что каждый последующий сертификат в цепочке подмахан предыдущим, и в конце концов упирается в корневой сертификат из нашего доверенного списка (opts.Roots). Если где-то звено сомнительное — всё, пиздец, доверия ебать ноль.
  • Не просрочен ли, сука: Сверяет даты — текущее время должно быть между NotBefore (не раньше) и NotAfter (не позже) для каждой бумажки в цепочке. А то вдруг он вчера сдох, а мы и не в курсе.
  • Имя, ёпта: Если в opts.DNSName что-то указали, проверяет, а выдан ли сертификат именно на этот домен. Чтобы к «google.com» не лез какой-нибудь «pornhub.biz» со своим удостоверением.
  • Цель использования: Смотрит, можно ли этот сертификат вообще использовать для серверной аутентификации. Мало ли он только для подписи почты или для открывания дверей в сортире.

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