среда, 11 марта 2020 г.

Node.js HTTPS SSL Сертификаты

Обычно HTTPS сервер делают базовое TLS-рукопожатие и принимают любые клиентские запросы пока может быть найден подходящий шифр. Однако сервер может быть настрое на вызов клиентского CertificateRequest в процессе TLS-рукопожатия.
Это заставляет клиента предоставить валидный сертификат прежде чем можно будет продолжить соединение.

Давайте пройдемся по процессу создания сертификатов вместе с сервером и клиентом использующим их.
Сначала мы создадим Certificate Authority (CA) для возможности подписания наших клиентский сертификатов. (Также давайте используем Certificate Authority (CA) для подписания сертификата для сервера.)
Для упрощения процесса конфигурирования Certificate Authority (CA) создадим следующий конфигурационный файл ca.cnf со следующим содержимым:

[ ca ]
default_ca      = CA_default

[ CA_default ]
serial = ca-serial
crl = ca-crl.pem
database = ca-database.txt
name_opt = CA_default
cert_opt = CA_default
default_crl_days = 9999
default_md = md5

[ req ]
default_bits           = 4096
days                   = 9999
distinguished_name     = req_distinguished_name
attributes             = req_attributes
prompt                 = no
output_password        = password

[ req_distinguished_name ]
C                      = US
ST                     = MA
L                      = Boston
O                      = Example Co
OU                     = techops
CN                     = ca
emailAddress           = certs@example.com

[ req_attributes ]
challengePassword      = test
Далее мы создадим новую certificate authority, используя файл с этой конфигурацией с помощью команды:
openssl req -new -x509 -days 9999 -config ca.cnf -keyout ca-key.pem -out ca-crt.pem

После того, как после генерации мы получим certificate authority в файле ca-key.pem и ca-crt.pem мы создадим private key для нашего сервера командой:

openssl genrsa -out server-key.pem 4096
На следующем шаге мы сгенерируем certificate signing request

Снова для упрощения процесса конфигурирования мы создадим конфигурационный файл server.cnf со следующим соедержимым:

[ req ]
default_bits           = 4096
days                   = 9999
distinguished_name     = req_distinguished_name
attributes             = req_attributes
prompt                 = no
x509_extensions        = v3_ca

[ req_distinguished_name ]
C                      = US
ST                     = MA
L                      = Boston
O                      = Example Co
OU                     = techops
CN                     = localhost
emailAddress           = certs@example.com

[ req_attributes ]
challengePassword      = password

[ v3_ca ]
authorityInfoAccess = @issuer_info

[ issuer_info ]
OCSP;URI.0 = http://ocsp.example.com/
caIssuers;URI.0 = http://example.com/ca.cert
Теперь на основе этого конфигурационного файла мы сгенерируем certificate signing request командой:
openssl req -new -config server.cnf -key server-key.pem -out server-csr.pem

Теперь подпишем request командой:

openssl x509 -req -extfile server.cnf -days 999 -passin "pass:password" -in server-csr.pem -CA ca-crt.pem -CAkey ca-key.pem -CAcreateserial -out server-crt.pem
В результате этого сертификат для нашего сервер будет готов.

Сервер.

Создадим простой Node.js HTTPS сервер, используя сертификат, который будет доступен по адресу 0.0.0.0:4433

var fs = require('fs'); 
var https = require('https'); 
var options = { 
    key: fs.readFileSync('server-key.pem'), 
    cert: fs.readFileSync('server-crt.pem'), 
    ca: fs.readFileSync('ca-crt.pem'), 
}; 
https.createServer(options, function (req, res) { 
    console.log(new Date() + ' ' + 
        req.connection.remoteAddress + ' ' + 
        req.method + ' ' + req.url); 
    res.writeHead(200); 
    res.end("hello world\n"); 
}).listen(4433);
Вы можете проверить работу сервера через браузер. Не забудьте в сообщить вашей операционной системе доверять созданной намиcertificate через установку файла ca-crt.pem и пометки его, как trusted.

Клиент.

Давайте создадим клиента для соединения с сервером с целью демонстрации работы с клиентским сертификатом.

var fs = require('fs'); 
var https = require('https'); 
var options = { 
    hostname: 'localhost', 
    port: 4433, 
    path: '/', 
    method: 'GET', 
    ca: fs.readFileSync('ca-crt.pem') 
}; 
var req = https.request(options, function(res) { 
    res.on('data', function(data) { 
        process.stdout.write(data); 
    }); 
}); 

req.end();
Мы добавили объект options, в котором установили certificate authority (ca) для того, чтобы клиент доверял public key нашего certificate authority. Для проверки вы можете запустить код клиента, чтобы увидеть, как сервер ответит ему фразой "hello world".

Клиентские сертификаты.

Теперь создадим сертификаты для клиента, которые он будет передавать серверу, следующими командами:

openssl genrsa -out client1-key.pem 4096
openssl genrsa -out client2-key.pem 4096

Для удобства создания сертификатом опять создадим вспомогательные конфигурационные файлы.

Файл client1.cnf со следующим содержимым:

[ req ]
default_bits           = 4096
days                   = 9999
distinguished_name     = req_distinguished_name
attributes             = req_attributes
prompt                 = no
x509_extensions        = v3_ca

[ req_distinguished_name ]
C                      = US
ST                     = MA
L                      = Boston
O                      = Example Co
OU                     = techops
CN                     = client1
emailAddress           = certs@example.com

[ req_attributes ]
challengePassword      = password

[ v3_ca ]
authorityInfoAccess = @issuer_info

[ issuer_info ]
OCSP;URI.0 = http://ocsp.example.com/
caIssuers;URI.0 = http://example.com/ca.cert

И файл client2.cnf со следующим содержимым:

[ req ]
default_bits           = 4096
days                   = 9999
distinguished_name     = req_distinguished_name
attributes             = req_attributes
prompt                 = no
x509_extensions        = v3_ca

[ req_distinguished_name ]
C                      = US
ST                     = MA
L                      = Boston
O                      = Example Co
OU                     = techops
CN                     = client2
emailAddress           = certs@example.com

[ req_attributes ]
challengePassword      = password

[ v3_ca ]
authorityInfoAccess = @issuer_info

[ issuer_info ]
OCSP;URI.0 = http://ocsp.example.com/

caIssuers;URI.0 = http://example.com/ca.cert

Теперь выполним команды для создания двух certificate signing requests:

openssl req -new -config client1.cnf -key client1-key.pem -out client1-csr.pem

openssl req -new -config client2.cnf -key client2-key.pem -out client2-csr.pem

И подпишем наши два клиентских сертификата следующими командами:

openssl x509 -req -extfile client1.cnf -days 999 -passin "pass:password" -in client1-csr.pem -CA ca-crt.pem -CAkey ca-key.pem -CAcreateserial -out client1-crt.pem

openssl x509 -req -extfile client2.cnf -days 999 -passin "pass:password" -in client2-csr.pem -CA ca-crt.pem -CAkey ca-key.pem -CAcreateserial -out client2-crt.pem

Для того, чтобы убедиться, что всё получилось правильно проверим наши сертификаты следующими командами:

openssl verify -CAfile ca-crt.pem client1-crt.pem
openssl verify -CAfile ca-crt.pem client2-crt.pem

В результате мы должны получить “OK”, если всё сделано верно.

Проверка работы клиента и сервера вместе.

Немного изменим настройки сервера для того, установив requestCert и rejectUnauthorized в true, в options для того, чтобы разрешать запросы от клиента только по сертификатам.

var fs = require('fs'); 
var https = require('https'); 
var options = { 
    key: fs.readFileSync('server-key.pem'), 
    cert: fs.readFileSync('server-crt.pem'), 
    ca: fs.readFileSync('ca-crt.pem'), 
    requestCert: true, 
    rejectUnauthorized: true
}; 
https.createServer(options, function (req, res) { 
    console.log(new Date() + ' ' + 
        req.connection.remoteAddress + ' ' + 
        req.socket.getPeerCertificate().subject.CN + ' ' + 
        req.method + ' ' + req.url); 
    res.writeHead(200); 
    res.end("hello world\n"); 

}).listen(4433);

Теперь, если вы произведете запрос к серверу из клиента без сертификата, то такой запрос сервером будет отвергнут из-за того, что валидный сертификат не будет передан. В Node.js в этом случае будет получена ошибка socket hangup error, из-за того, что TLS будет незавершенным.
Для того, чтобы всё заработало добавим в код клиента key и cert внутрь options.

var fs = require('fs'); 
var https = require('https'); 
var options = { 
    hostname: 'localhost', 
    port: 4433, 
    path: '/', 
    method: 'GET', 
    key: fs.readFileSync('client1-key.pem'), 
    cert: fs.readFileSync('client1-crt.pem'), 
    ca: fs.readFileSync('ca-crt.pem') 
}; 
var req = https.request(options, function(res) { 
    res.on('data', function(data) { 
        process.stdout.write(data); 
    }); 
}); 
req.end(); 
req.on('error', function(e) { 
    console.error(e); 

});

После этого при соединении клиента с сервером клиент в ответ полуичт строку "hello world". А на стороне сервера в это время в консоль будет выведен лог  со строкой "client1", поскольку сервер сможет распознать значение "Common Name", полученное из сертификата клиента.

Если вы попробуете заменить сертификат на клиенте с client1 на client2, тогда вы увидите, как сервер выведет в консоль распознанное "client2".

Отзыв сертификата.

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

Для того, чтобы это сделать мы создадим Certificate Revocation List (CRL) и отзовем клиентский сертификат client2.
Прежде чем это сделать мы создадим  пустой текстовый файл:

ca-database.txt

Теперь отзовем клиентский сертификат client2 следующей командой:

openssl ca -revoke client2-crt.pem -keyfile ca-key.pem -config ca.cnf -cert ca-crt.pem -passin 'pass:password'

И далее обновим CRL следующей командой:

openssl ca -keyfile ca-key.pem -cert ca-crt.pem -config ca.cnf -gencrl -out ca-crl.pem -passin 'pass:password'

Наконец определим отозванный сертификат внутри crl внутри options нашего сервера:

var fs = require('fs'); 
var https = require('https'); 
var options = { 
    key: fs.readFileSync('server-key.pem'), 
    cert: fs.readFileSync('server-crt.pem'), 
    ca: fs.readFileSync('ca-crt.pem'), 
    crl: fs.readFileSync('ca-crl.pem'), 
    requestCert: true, 
    rejectUnauthorized: true 
}; 
https.createServer(options, function (req, res) { 
    console.log(new Date()+' '+ 
        req.connection.remoteAddress+' '+ 
        req.socket.getPeerCertificate().subject.CN+' '+ 
        req.method+' '+req.url); 
    res.writeHead(200); 
    res.end("hello world\n"); 

}).listen(4433);

Теперь наш сервер будет учитывать список отозванных сертификатов, благодаря чему запросы от клиента с сертификатом client2 будут сервером отвергаться, а запросы от клиента с сертификатом client1, так же, как и раньше будут приниматься.
Итак, мы увидели, как можно создавать самоподписанные сертификаты (self signed certificates) для клиента и сервера и убедились, что клиенты могут взаимодействовать с сервером, используя только валидные сертификаты, подписанные нами.
Дополнительно мы научились отзывать сертификаты.
Поскольку на сервер мы может видеть значение "Common Name" из сертификата клиента, то мы можем использовать его для идентификации подключившегося клиента на нашем сервере.

engineering.circle.com/https-authorized-certs-with-node-js-315e548354a2

Комментариев нет:

Отправить комментарий