При попытках организовать https-соединения для различных web-сервисов с использованием ГОСТ-шифрования всегда оставались вопросы с посетителями, браузеры которых не поддерживают ГОСТ-алгоритмы. Логичным казалось решение при установке https-соединения отдавать клиенту сертификат в зависимости от поддерживаемых его системой алгоритмов, но до недавнего времени практические реализации такого подхода мне не встречались.
Когда-то я прочитал статью kyprizel «Как и зачем мы делаем TLS в Яндексе», где упоминалось о том, что OpenSSL начиная с версии 1.0.2 позволяет назначать сертификат сервера в зависимости от параметров клиента, но реализации на стороне Web-сервера нет. В Nginx 1.11.0 появилась такая возможность:
директивы ssl_certificate и ssl_certificate_key теперь можно указывать несколько раз для загрузки сертификатов разных типов (например, RSA и ECDSA).
Я решил собрать стенд с целью протестировать возможность организации https web-сервера c сертификатами ГОСТ для посетителей с установленным крипопровайдером ГОСТ-шифрования и сертификатами ECDSA для остальных.
В качестве тестового стенда выступила VM с Ubuntu 16.04.1 LTS
Собираем nginx
Я собирал nginx со статической библиотекой OpenSSL 1.0.2h
cd /opt/src/
#Скачиваем nginx
wget http://nginx.org/download/nginx-1.11.2.tar.gz
#Скачиваем openssl
wget https://openssl.org/source/openssl-1.0.2h.tar.gz
#Распаковываем
tar -zxvf nginx-1.11.2.tar.gz
tar -zxvf openssl-1.0.2h.tar.gz
cd nginx-1.11.2
#И собираем
./configure --prefix=/opt/work/nginx2 --user=nginx --group=nginx --with-http_ssl_module --with-openssl=/opt/src/openssl-1.0.2h/
make
make install
Далее необходимо сконфигурировать OpenSSL для поддержки алгоритмов ГОСТ. В сети даже ленивый сможет найти материалы по настройке.
cat /opt/src/openssl-1.0.2h/.openssl/ssl/openssl.cnf
openssl_conf=openssl_def
HOME = .
RANDFILE = $ENV::HOME/.rnd
oid_section = new_oids
[ new_oids ]
tsa_policy1 = 1.2.3.4.1
tsa_policy2 = 1.2.3.4.5.6
tsa_policy3 = 1.2.3.4.5.7
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = ./demoCA
certs = $dir/certs
crl_dir = $dir/crl
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/cacert.pem
serial = $dir/serial
crlnumber = $dir/crlnumber
crl = $dir/crl.pem
private_key = $dir/private/cakey.pem
RANDFILE = $dir/private/.rand
x509_extensions = usr_cert
name_opt = ca_default
cert_opt = ca_default
default_days = 365
default_crl_days= 30
default_md = default
preserve = no
policy = policy_match
[ policy_match ]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ policy_anything ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
default_bits = 2048
default_keyfile = privkey.pem
distinguished_name = req_distinguished_name
attributes = req_attributes
req_extensions = v3_req
x509_extensions = v3_ca
string_mask = utf8only
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = RU
countryName_min = 2
countryName_max = 2
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = Moscow region
localityName = Locality Name (eg, city)
localityName_default = Moscow
0.organizationName = Organization Name (eg, company)
0.organizationName_default = JSC Example
organizationalUnitName = Organizational Unit Name (eg, section)
organizationalUnitName_default = It Department
commonName = Common Name (e.g. server FQDN or YOUR name)
commonName_max = 64
emailAddress = Email Address
emailAddress_max = 64
[ req_attributes ]
challengePassword = A challenge password
challengePassword_min = 4
challengePassword_max = 20
unstructuredName = An optional company name
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = test.example.ru
DNS.2 = gost.example.ru
[ usr_cert ]
basicConstraints=CA:FALSE
nsComment = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = CA:true
[ crl_ext ]
authorityKeyIdentifier=keyid:always
[ proxy_cert_ext ]
basicConstraints=CA:FALSE
nsComment = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo
[ tsa ]
default_tsa = tsa_config1
[ tsa_config1 ]
dir = ./demoCA
serial = $dir/tsaserial
crypto_device = builtin
signer_cert = $dir/tsacert.pem
certs = $dir/cacert.pem
signer_key = $dir/private/tsakey.pem
default_policy = tsa_policy1
other_policies = tsa_policy2, tsa_policy3
digests = md5, sha1
accuracy = secs:1, millisecs:500, microsecs:100
clock_precision_digits = 0
ordering = yes
tsa_name = yes
ess_cert_id_chain = no
[openssl_def]
engines = engine_section
[engine_section]
gost = gost_section
[gost_section]
engine_id = gost
default_algorithms = ALL
CRYPT_PARAMS = id-Gost28147-89-CryptoPro-A-ParamSet
Выпускаем сертификаты для тестового web-ресурса. Я не стал отягощать себя самоподписанными сертификатами, а создал запросы и подписал их в Тестовом УЦ КриптоПРО и у Китайских «друзей», уже давно выдающих бесплатные сертификаты.
#Формируем закрытый ключ
openssl genrsa -out test.example.ru.key 2048
#генерируем запрос
openssl req -new -sha256 -key test.example.ru.key -out test.example.ru.csr
#генерирум закрытый ключ по алгоритму ГОСТ
openssl genpkey -algorithm gost2001 -pkeyopt paramset:A -out gost.example.ru.key
#генерируем запрос
openssl req -engine gost -new -key gost.example.ru.key -out gost.example.ru.csr
Выгружаем полученные запросы и подписываем в УЦ.
Конфигурирование Nginx
Ниже приведен мой конфигурационный файл Web-сервера Nginx. Следует обратить внимание на дублирующиеся директивы ssl_certificate и ssl_certificate_key, в которых задаются 2 сертификата для одного https сервера, а также на строку ssl_ciphers GOST2001-GOST89-GOST89:HIGH:MEDIUM , где определяется список и порядок применяемых алгоритмов шифрования.
user nginx;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 443 ssl;
server_name gost.example.ru;
ssl_certificate keys/gost.example.ru_bundle.crt;
ssl_certificate_key keys/gost.example.ru.key;
ssl_certificate keys/test.example.ru_bundle.crt;
ssl_certificate_key keys/test.example.ru.key;
ssl_ciphers GOST2001-GOST89-GOST89:HIGH:MEDIUM;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://192.168.1.249;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
На просторах паутины я нашел init script для более удобного запуска:
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DESC="Nginx Daemon"
NAME=nginx
PREFIX=/opt/work/nginx2
DAEMON=$PREFIX/sbin/$NAME
CONF=$PREFIX/conf/$NAME.conf
PID=$PREFIX/logs/$NAME.pid
SCRIPT=/etc/init.d/$NAME
if [ ! -x "$DAEMON" ] || [ ! -f "$CONF" ]; then
echo -e "33[33m $DAEMON has no permission to run. 33[0m"
echo -e "33[33m Or $CONF doesn't exist. 33[0m"
sleep 1
exit 1
fi
do_start() {
if [ -f $PID ]; then
echo -e "33[33m $PID already exists. 33[0m"
echo -e "33[33m $DESC is already running or crashed. 33[0m"
echo -e "33[32m $DESC Reopening $CONF ... 33[0m"
$DAEMON -s reopen -c $CONF
sleep 1
echo -e "33[36m $DESC reopened. 33[0m"
else
echo -e "33[32m $DESC Starting $CONF ... 33[0m"
$DAEMON -c $CONF
sleep 1
echo -e "33[36m $DESC started. 33[0m"
fi
}
do_stop() {
if [ ! -f $PID ]; then
echo -e "33[33m $PID doesn't exist. 33[0m"
echo -e "33[33m $DESC isn't running. 33[0m"
else
echo -e "33[32m $DESC Stopping $CONF ... 33[0m"
$DAEMON -s stop -c $CONF
sleep 1
echo -e "33[36m $DESC stopped. 33[0m"
fi
}
do_reload() {
if [ ! -f $PID ]; then
echo -e "33[33m $PID doesn't exist. 33[0m"
echo -e "33[33m $DESC isn't running. 33[0m"
echo -e "33[32m $DESC Starting $CONF ... 33[0m"
$DAEMON -c $CONF
sleep 1
echo -e "33[36m $DESC started. 33[0m"
else
echo -e "33[32m $DESC Reloading $CONF ... 33[0m"
$DAEMON -s reload -c $CONF
sleep 1
echo -e "33[36m $DESC reloaded. 33[0m"
fi
}
do_quit() {
if [ ! -f $PID ]; then
echo -e "33[33m $PID doesn't exist. 33[0m"
echo -e "33[33m $DESC isn't running. 33[0m"
else
echo -e "33[32m $DESC Quitting $CONF ... 33[0m"
$DAEMON -s quit -c $CONF
sleep 1
echo -e "33[36m $DESC quitted. 33[0m"
fi
}
do_test() {
echo -e "33[32m $DESC Testing $CONF ... 33[0m"
$DAEMON -t -c $CONF
}
do_info() {
$DAEMON -V
}
case "$1" in
start)
do_start
;;
stop)
do_stop
;;
reload)
do_reload
;;
restart)
do_stop
do_start
;;
quit)
do_quit
;;
test)
do_test
;;
info)
do_info
;;
*)
echo "Usage: $SCRIPT {start|stop|reload|restart|quit|test|info}"
exit 2
;;
esac
exit 0
Все, запускаем веб-сервер и проверяем. В браузере Internet Explorer 11 c установленным криптопровайдером КриптоПро CSP посетителю отдается ГОСТ сертификат, подписанный CRYPTO-PRO Test Center 2 при обращении к gost.example.ru, в Mozilla Firefox нет поддержки алгоритмов ГОСТ и посетитель получает «обычный» сертификат, подписанный WoSign CA Limited.
Internet Explorer
Mozilla Firefox
С моей точки зрения получился достаточно интересный вариант использования технологии, хочется надеяться, что скоро появится поддержка в openresty ssl_certificate_by_lua_block.
Автор: mrdoger