[Из песочницы] Универсальный https c использованием ГОСТ сертификата

При попытках организовать 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 для поддержки алгоритмов ГОСТ. В сети даже ленивый сможет найти материалы по настройке.
мой openssl.cnf
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 для более удобного запуска:

/etc/init.d/nginx
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 "\033[33m $DAEMON has no permission to run. \033[0m"
    echo -e "\033[33m Or $CONF doesn't exist. \033[0m"
    sleep 1
    exit 1
fi

do_start() {
    if [ -f $PID ]; then
        echo -e "\033[33m $PID already exists. \033[0m"
        echo -e "\033[33m $DESC is already running or crashed. \033[0m"
        echo -e "\033[32m $DESC Reopening $CONF ... \033[0m"
        $DAEMON -s reopen -c $CONF
        sleep 1
        echo -e "\033[36m $DESC reopened. \033[0m"
    else
        echo -e "\033[32m $DESC Starting $CONF ... \033[0m"
        $DAEMON -c $CONF
        sleep 1
        echo -e "\033[36m $DESC started. \033[0m"
    fi
}

do_stop() {
    if [ ! -f $PID ]; then
        echo -e "\033[33m $PID doesn't exist. \033[0m"
        echo -e "\033[33m $DESC isn't running. \033[0m"
    else
        echo -e "\033[32m $DESC Stopping $CONF ... \033[0m"
        $DAEMON -s stop -c $CONF
        sleep 1
        echo -e "\033[36m $DESC stopped. \033[0m"
    fi
}

do_reload() {
    if [ ! -f $PID ]; then
        echo -e "\033[33m $PID doesn't exist. \033[0m"
        echo -e "\033[33m $DESC isn't running. \033[0m"
        echo -e "\033[32m $DESC Starting $CONF ... \033[0m"
        $DAEMON -c $CONF
        sleep 1
        echo -e "\033[36m $DESC started. \033[0m"
    else
        echo -e "\033[32m $DESC Reloading $CONF ... \033[0m"
        $DAEMON -s reload -c $CONF
        sleep 1
        echo -e "\033[36m $DESC reloaded. \033[0m"
    fi
}

do_quit() {
    if [ ! -f $PID ]; then
        echo -e "\033[33m $PID doesn't exist. \033[0m"
        echo -e "\033[33m $DESC isn't running. \033[0m"
    else
        echo -e "\033[32m $DESC Quitting $CONF ... \033[0m"
        $DAEMON -s quit -c $CONF
        sleep 1
        echo -e "\033[36m $DESC quitted. \033[0m"
    fi
}

do_test() {
    echo -e "\033[32m $DESC Testing $CONF ... \033[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
6fd24c11e41b4ae49e356dea1fc9e93b.pngMozilla Firefox
e7f19eedbe714b0e9a3bffe7627e10a6.png

С моей точки зрения получился достаточно интересный вариант использования технологии, хочется надеяться, что скоро появится поддержка в openresty ssl_certificate_by_lua_block.

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

© Habrahabr.ru