О том, что поле CN в TLS-сертификатах помечено устаревшим ещё в 2000… — Остальные 90% — TG.ME

О том, что поле CN в TLS-сертификатах помечено устаревшим ещё в 2000 году, я читал не раз. И всё-таки неожиданным открытием для меня оказалось, что HTTPS-клиенты, написанные на Go 1.17+, отказываются работать с такими сертификатами совершенно.

Открытием это стало, поскольку тот же curl, с которым обычно приходится иметь дело, ведёт себя иначе. Чтобы в этом убедиться, создадим ключ и сертификат, выписав его на имя localhost, и запустим с ними простой веб-сервер:
# Creates files cert.pem and key.pem
$ openssl req -x509 -keyout key.pem -out cert.pem \
-days 365 -nodes -subj '/CN=localhost/'

# Check there is no SAN in certificate
$ openssl x509 -in cert.pem -noout -ext subjectAltName
No extensions in certificate

$ openssl s_server -key key.pem -cert cert.pem -WWW -port 4433

Веб-сервер будет отдавать файлы из рабочей директории, так что для тестирования запросим любой из имеющихся - например, cert.pem.

В соседнем терминале подключимся к серверу при помощи curl. Поскольку сертификат, который отдаёт сервер, самоподписанный, используем его же для валидации подключения:
$ curl https://localhost:4433/cert.pem --cacert cert.pem
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUO6d0vnNmNST77...
...

Как мы видим, сертификат вполне удовлетворил curl. Теперь попробуем обратиться к тому же серверу, но по другому имени:
# Using '--resolve' to avoid messing with /etc/hosts
$ curl https://example.com:4433/cert.pem --cacert cert.pem \
--resolve 'example.com:4433:127.0.0.1'
curl: (60) SSL: certificate subject name 'localhost' does not match
target host name 'example.com'

Как и ожидалось, теперь сертификат не принимается - ничего необычного.

Возьмём теперь HTTPS-клиента, написанного на Go, который будет подключаться к тому же серверу. Убедимся, что версия go не менее 1.17, и запустим клиент:
$ go version
go version go1.22.1 linux/amd64

$ CERT=cert.pem URL='https://localhost:4433/' go run main.go
2025/12/22 00:33:23 Error making request: Get "https://localhost:4433/":
tls: failed to verify certificate:
x509: certificate relies on legacy Common Name field, use SANs instead
exit status 1

Несмотря на соответствие доменного имени и сертификата клиент отказался подключаться к серверу.

В версиях go до 1.16 включительно это поведение можно было переопределить, указав переменную GODEBUG=x509ignoreCN=0, однако уже с версии 1.17 изменить поведение стандартной библиотеки нельзя. Единственный способ заставить клиента подключиться - выписать сертификат, содержащий соответствующее поле SAN. Перевыпишем сертификат crt.pem с ключом -addext и перезапустим сервер:
$ openssl req -x509 -keyout key.pem -out cert.pem \
-days 365 -nodes -subj '/CN=localhost/' \
-addext 'subjectAltName = DNS:localhost'

# Check SAN exists now
$ openssl x509 -in cert.pem -noout -ext subjectAltName
X509v3 Subject Alternative Name:
DNS:localhost

$ openssl s_server -key key.pem -cert cert.pem -WWW -port 4433


Теперь клиент сможет подключиться и также загрузить файл:
$ CERT=cert.pem URL='https://localhost:4433/' go run main.go 
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUO6d0vnNmNST77...
...


Оставляя за скобками недовольство по поводу такого неоднозначного изменения в Go (проблему решали день), выводы хочется сделать следующие:
1. необходимо внимательно следить за содержимым сертификатов - расширения уже давно стали неотъемлемой частью сертификата;
2. openssl по умолчанию выписывает сертификат, который не принимается многими клиентами - важно помнить о ключе -addext.

Кстати, mkcert, имеющийся в репозиториях Ubuntu, создаст ту же пару файлов при при помощи короткой команды mkcert localhost, однако сразу с расширением SAN и не требуя запоминать десяток неочевидных ключей. В этом вопросе он однозначно обходит openssl :)
👍14❤4🔥4
December 22, 2025 875 16