модуль OpenSSL
OpenSSL предоставляет SSL, TLS и криптографию общего назначения. Он оборачивает библиотеку OpenSSL.
Примеры
Все примеры предполагают, что вы загрузили OpenSSL с помощью:
require 'openssl'
Эти примеры строятся друг на друге. Например, ключ, созданный в следующем примере, используется во всех этих примерах.
Ключи
Создание ключа
Этот пример создает пару ключей RSA размером 2048 бит и сохраняет её в текущем каталоге.
key = OpenSSL::PKey::RSA.new 2048 File.write 'private_key.pem', key.private_to_pem File.write 'public_key.pem', key.public_to_pem
Экспорт ключа
Ключи, сохранённые на диске без шифрования, не являются безопасными, так как любой, получивший ключ, может его использовать, если он не зашифрован. Для безопасного экспорта ключа вы можете экспортировать его с паролем.
cipher = OpenSSL::Cipher.new 'aes-256-cbc' password = 'my secure password goes here' key_secure = key.private_to_pem cipher, password File.write 'private.secure.pem', key_secure
OpenSSL::Cipher.ciphers возвращает список доступных шифров.
Загрузка ключа
Ключ также можно загрузить из файла.
key2 = OpenSSL::PKey.read File.read 'private_key.pem' key2.public? # => true key2.private? # => true
или
key3 = OpenSSL::PKey.read File.read 'public_key.pem' key3.public? # => true key3.private? # => false
Загрузка зашифрованного ключа
OpenSSL запросит у вас пароль при загрузке зашифрованного ключа. Если вы не сможете ввести пароль, вы можете указать его при загрузке ключа:
key4_pem = File.read 'private.secure.pem' password = 'my secure password goes here' key4 = OpenSSL::PKey.read key4_pem, password
Шифрование RSA
RSA предоставляет шифрование и дешифрование с использованием открытого и закрытого ключей. Вы можете использовать различные методы дополнения в зависимости от предполагаемого использования зашифрованных данных.
Шифрование и дешифрование
Асимметричное шифрование с открытым/закрытым ключом медленное и уязвимо к атакам в случаях, когда оно используется без дополнения или напрямую для шифрования больших фрагментов данных. Типичные случаи использования шифрования RSA включают «обёртку» симметричного ключа с открытым ключом получателя, который «разоберёт» этот симметричный ключ с помощью своего закрытого ключа. Однако это не следует использовать на практике; всегда следует отдавать предпочтение стандартизированным протоколам.
wrapped_key = key.public_encrypt key
Симметричный ключ, зашифрованный с помощью открытого ключа, можно расшифровать только с помощью соответствующего закрытого ключа получателя.
original_key = key.private_decrypt wrapped_key
По умолчанию используется дополнение PKCS#1, но также возможно использование других форм дополнения, см. PKey::RSA для получения дополнительной информации.
Подписи
Использование «private_encrypt» для шифрования некоторых данных с помощью закрытого ключа эквивалентно применению цифровой подписи к данным. Проверяющая сторона может проверить подпись, сравнив результат дешифрования подписи с помощью «public_decrypt» с исходными данными. Однако OpenSSL::PKey уже имеет методы «sign» и «verify», которые обрабатывают цифровые подписи стандартизованным способом — «private_encrypt» и «public_decrypt» не следует использовать на практике.
Для подписи документа сначала вычисляется криптографически безопасный хэш документа, который затем подписывается с помощью закрытого ключа.
signature = key.sign 'SHA256', document
Для проверки подписи снова вычисляется хэш документа, и подпись расшифровывается с использованием открытого ключа. Результат затем сравнивается с только что вычисленным хэшем; если они равны, подпись действительна.
if key.verify 'SHA256', signature, document puts 'Valid' else puts 'Invalid' end
Шифрование с паролем PBKDF2
Если это поддерживается используемой версией OpenSSL, шифрование с паролем должно использовать возможности PKCS5. Если это не поддерживается или если этого требуют устаревшие приложения, также поддерживаются более старые, менее безопасные методы, указанные в RFC 2898 (см. ниже).
PKCS5 поддерживает PBKDF2, как это определено в PKCS#5 v2.0. Он по-прежнему использует пароль, соль и, дополнительно, количество итераций, которые замедлят процесс вывода ключа. Чем медленнее это происходит, тем больше работы требуется, чтобы взломать полученный ключ.
Шифрование
Стратегия заключается в том, чтобы сначала создать Cipher для шифрования, а затем сгенерировать случайный IV и ключ, полученный из пароля с помощью PBKDF2. PKCS #5 v2.0 рекомендует использовать не менее 8 байтов для соли; количество итераций в значительной степени зависит от используемого оборудования.
cipher = OpenSSL::Cipher.new 'aes-256-cbc'
cipher.encrypt
iv = cipher.random_iv
pwd = 'some hopefully not to easily guessable password'
salt = OpenSSL::Random.random_bytes 16
iter = 20000
key_len = cipher.key_len
digest = OpenSSL::Digest.new('SHA256')
key = OpenSSL::PKCS5.pbkdf2_hmac(pwd, salt, iter, key_len, digest)
cipher.key = key
Now encrypt the data:
encrypted = cipher.update document
encrypted << cipher.final Расшифровка
Используйте те же шаги, что и раньше, для вывода симметричного ключа AES, но на этот раз настройте Cipher для расшифрования.
cipher = OpenSSL::Cipher.new 'aes-256-cbc'
cipher.decrypt
cipher.iv = iv # the one generated with #random_iv
pwd = 'some hopefully not to easily guessable password'
salt = ... # the one generated above
iter = 20000
key_len = cipher.key_len
digest = OpenSSL::Digest.new('SHA256')
key = OpenSSL::PKCS5.pbkdf2_hmac(pwd, salt, iter, key_len, digest)
cipher.key = key
Now decrypt the data:
decrypted = cipher.update encrypted
decrypted << cipher.final Сертификаты
Создание сертификата
В этом примере создается самоподписанный сертификат с использованием ключа RSA и подписи SHA1.
key = OpenSSL::PKey::RSA.new 2048 name = OpenSSL::X509::Name.parse '/CN=nobody/DC=example' cert = OpenSSL::X509::Certificate.new cert.version = 2 cert.serial = 0 cert.not_before = Time.now cert.not_after = Time.now + 3600 cert.public_key = key.public_key cert.subject = name
Расширения сертификата
Вы можете добавить расширения к сертификату с помощью OpenSSL::SSL::ExtensionFactory, чтобы указать назначение сертификата.
extension_factory = OpenSSL::X509::ExtensionFactory.new nil, cert
cert.add_extension \
extension_factory.create_extension('basicConstraints', 'CA:FALSE', true)
cert.add_extension \
extension_factory.create_extension(
'keyUsage', 'keyEncipherment,dataEncipherment,digitalSignature')
cert.add_extension \
extension_factory.create_extension('subjectKeyIdentifier', 'hash')
Список поддерживаемых расширений (и в некоторых случаях их возможные значения) можно получить из файла «objects.h» в исходном коде OpenSSL.
Подписание сертификата
Чтобы подписать сертификат, установите издателя и используйте OpenSSL::X509::Certificate#sign с алгоритмом дайджеста. Это создаёт самоподписанный сертификат, поскольку мы используем то же имя и ключ для подписи сертификата, что и для создания сертификата.
cert.issuer = name
cert.sign key, OpenSSL::Digest.new('SHA1')
open 'certificate.pem', 'w' do |io| io.write cert.to_pem end
Загрузка сертификата
Как и ключ, сертификат также можно загрузить из файла.
cert2 = OpenSSL::X509::Certificate.new File.read 'certificate.pem'
Проверка сертификата
Certificate#verify вернет true, когда сертификат был подписан данным открытым ключом.
raise 'certificate can not be verified' unless cert2.verify key
Центр сертификации
Центр сертификации (ЦС) — это доверенная третья сторона, которая позволяет вам проверять владение неизвестными сертификатами. ЦС выдает подписи ключей, которые указывают на то, что он доверяет пользователю этого ключа. Пользователь, столкнувшийся с ключом, может проверить подпись, используя открытый ключ ЦС.
Ключ ЦС
Ключи ЦС ценны, поэтому мы шифруем и сохраняем их на диске и убеждаемся, что они недоступны другим пользователям.
ca_key = OpenSSL::PKey::RSA.new 2048 password = 'my secure password goes here' cipher = 'aes-256-cbc' open 'ca_key.pem', 'w', 0400 do |io| io.write ca_key.private_to_pem(cipher, password) end
Сертификат ЦС
Сертификат ЦС создаётся так же, как мы создавали сертификат выше, но с другими расширениями.
ca_name = OpenSSL::X509::Name.parse '/CN=ca/DC=example'
ca_cert = OpenSSL::X509::Certificate.new
ca_cert.serial = 0
ca_cert.version = 2
ca_cert.not_before = Time.now
ca_cert.not_after = Time.now + 86400
ca_cert.public_key = ca_key.public_key
ca_cert.subject = ca_name
ca_cert.issuer = ca_name
extension_factory = OpenSSL::X509::ExtensionFactory.new
extension_factory.subject_certificate = ca_cert
extension_factory.issuer_certificate = ca_cert
ca_cert.add_extension \
extension_factory.create_extension('subjectKeyIdentifier', 'hash')
Это расширение указывает, что ключ ЦС может использоваться как ключ ЦС.
ca_cert.add_extension \
extension_factory.create_extension('basicConstraints', 'CA:TRUE', true)
Это расширение указывает, что ключ ЦС может использоваться для проверки подписей как на сертификатах, так и на отзывах сертификатов.
ca_cert.add_extension \
extension_factory.create_extension(
'keyUsage', 'cRLSign,keyCertSign', true)
Корневые сертификаты ЦС самоподписываются.
ca_cert.sign ca_key, OpenSSL::Digest.new('SHA1')
Сертификат ЦС сохраняется на диске, чтобы его могли использовать все пользователи ключей, которые подписывает этот ЦС.
open 'ca_cert.pem', 'w' do |io| io.write ca_cert.to_pem end
Запрос на подпись сертификата
ЦС подписывает ключи через запрос на подпись сертификата (CSR). CSR содержит информацию, необходимую для идентификации ключа.
csr = OpenSSL::X509::Request.new
csr.version = 0
csr.subject = name
csr.public_key = key.public_key
csr.sign key, OpenSSL::Digest.new('SHA1')
CSR сохраняется на диске и отправляется в ЦС для подписи.
open 'csr.pem', 'w' do |io| io.write csr.to_pem end
Создание сертификата из CSR
После получения CSR ЦС проверит его перед подписью. Минимальная проверка — проверка подписи CSR.
csr = OpenSSL::X509::Request.new File.read 'csr.pem' raise 'CSR can not be verified' unless csr.verify csr.public_key
После проверки создаётся сертификат, помечаемый для различных применений, подписывается с помощью ключа ЦС и возвращается запрашивающему.
csr_cert = OpenSSL::X509::Certificate.new
csr_cert.serial = 0
csr_cert.version = 2
csr_cert.not_before = Time.now
csr_cert.not_after = Time.now + 600
csr_cert.subject = csr.subject
csr_cert.public_key = csr.public_key
csr_cert.issuer = ca_cert.subject
extension_factory = OpenSSL::X509::ExtensionFactory.new
extension_factory.subject_certificate = csr_cert
extension_factory.issuer_certificate = ca_cert
csr_cert.add_extension \
extension_factory.create_extension('basicConstraints', 'CA:FALSE')
csr_cert.add_extension \
extension_factory.create_extension(
'keyUsage', 'keyEncipherment,dataEncipherment,digitalSignature')
csr_cert.add_extension \
extension_factory.create_extension('subjectKeyIdentifier', 'hash')
csr_cert.sign ca_key, OpenSSL::Digest.new('SHA1')
open 'csr_cert.pem', 'w' do |io|
io.write csr_cert.to_pem
end
SSL и TLS-соединения
Используя созданный нами ключ и сертификат, мы можем создать SSL или TLS-соединение. Для настройки SSL сессии используется SSLContext.
context = OpenSSL::SSL::SSLContext.new
SSL сервер
Для безопасного взаимодействия с клиентами SSL серверу требуется сертификат и закрытый ключ:
context.cert = cert context.key = key
Затем создайте SSLServer с сокетом сервера TCP и контекстом. Используйте SSLServer как обычный TCP-сервер.
require 'socket'
tcp_server = TCPServer.new 5000
ssl_server = OpenSSL::SSL::SSLServer.new tcp_server, context
loop do
ssl_connection = ssl_server.accept
data = ssl_connection.gets
response = "I got #{data.dump}"
puts response
ssl_connection.puts "I got #{data.dump}"
ssl_connection.close
end
SSL клиент
Клиент SSL создаётся с помощью сокета TCP и контекста. Необходимо вызвать SSLSocket#connect, чтобы инициировать SSL рукопожатие и начать шифрование. Клиентскому сокету не требуются ключ и сертификат.
Обратите внимание, что SSLSocket#close по умолчанию не закрывает подлежащий сокет. Установите SSLSocket#sync_close в true, если вам это нужно.
require 'socket' tcp_socket = TCPSocket.new 'localhost', 5000 ssl_client = OpenSSL::SSL::SSLSocket.new tcp_socket, context ssl_client.sync_close = true ssl_client.connect ssl_client.puts "hello server!" puts ssl_client.gets ssl_client.close # shutdown the TLS connection and close tcp_socket
Проверка узла
Непроверенное SSL соединение не обеспечивает достаточной безопасности. Для повышения безопасности клиент или сервер могут проверять сертификат своего узла.
Клиент может быть изменён для проверки сертификата сервера по отношению к сертификату центра сертификации:
context.ca_file = 'ca_cert.pem' context.verify_mode = OpenSSL::SSL::VERIFY_PEER require 'socket' tcp_socket = TCPSocket.new 'localhost', 5000 ssl_client = OpenSSL::SSL::SSLSocket.new tcp_socket, context ssl_client.connect ssl_client.puts "hello server!" puts ssl_client.gets
Если сертификат сервера недействителен или context.ca_file не задано при проверке узлов, будет поднято исключение OpenSSL::SSL::SSLError.
Константы
- LIBRESSL_VERSION_NUMBER
-
Номер версии LibreSSL, с которой был скомпилирован модуль Ruby
OpenSSL(в шестнадцатеричной системе счисления). Формат:0xMNNFF00f (major minor fix 00 status). Константа определена только для случаев LibreSSL.См. также страницу руководства
LIBRESSL_VERSION_NUMBER(3). - OPENSSL_FIPS
-
Булево значение, указывающее, поддерживает ли
OpenSSLрежим FIPS или нет. - OPENSSL_LIBRARY_VERSION
- OPENSSL_VERSION
- OPENSSL_VERSION_NUMBER
-
Номер версии
OpenSSLмодуля RubyOpenSSL(в шестнадцатеричной системе счисления). Ниже приведены форматы:-
OpenSSL3 -
0xMNN00PP0 (major minor 00 patch 0) -
OpenSSLдо 3 -
0xMNNFFPPS (major minor fix patch status) - LibreSSL
-
0x20000000 (fixed value)
См. также страницу руководства
OPENSSL_VERSION_NUMBER(3). -
- VERSION
Публичные методы класса
Исходный код
# File ext/openssl/lib/openssl/digest.rb, line 63 def Digest(name) OpenSSL::Digest.const_get(name) end
Возвращает подкласс Digest по имени.
require 'openssl'
OpenSSL::Digest("MD5")
# => OpenSSL::Digest::MD5
Digest("Foo")
# => NameError: wrong constant name Foo
Исходный код
static VALUE
ossl_debug_set(VALUE self, VALUE val)
{
dOSSL = RTEST(val) ? Qtrue : Qfalse;
return val;
} Включает или выключает режим отладки. В режиме отладки все ошибки, добавленные в очередь ошибок OpenSSL, будут выведены в stderr.
Исходный код
static VALUE
ossl_get_errors(VALUE _)
{
VALUE ary;
long e;
ary = rb_ary_new();
while ((e = ERR_get_error()) != 0){
rb_ary_push(ary, rb_str_new2(ERR_error_string(e, NULL)));
}
return ary;
} Просмотр оставшихся ошибок в очереди.
Любые ошибки, которые вы видите здесь, вероятно, являются ошибкой реализации OpenSSL в Ruby.
Исходный код
static VALUE
ossl_fips_mode_get(VALUE self)
{
#if OSSL_OPENSSL_PREREQ(3, 0, 0)
VALUE enabled;
enabled = EVP_default_properties_is_fips_enabled(NULL) ? Qtrue : Qfalse;
return enabled;
#elif defined(OPENSSL_FIPS)
VALUE enabled;
enabled = FIPS_mode() ? Qtrue : Qfalse;
return enabled;
#else
return Qfalse;
#endif
} Исходный код
static VALUE
ossl_fips_mode_set(VALUE self, VALUE enabled)
{
#if OSSL_OPENSSL_PREREQ(3, 0, 0)
if (RTEST(enabled)) {
if (!EVP_default_properties_enable_fips(NULL, 1)) {
ossl_raise(eOSSLError, "Turning on FIPS mode failed");
}
} else {
if (!EVP_default_properties_enable_fips(NULL, 0)) {
ossl_raise(eOSSLError, "Turning off FIPS mode failed");
}
}
return enabled;
#elif defined(OPENSSL_FIPS)
if (RTEST(enabled)) {
int mode = FIPS_mode();
if(!mode && !FIPS_mode_set(1)) /* turning on twice leads to an error */
ossl_raise(eOSSLError, "Turning on FIPS mode failed");
} else {
if(!FIPS_mode_set(0)) /* turning off twice is OK */
ossl_raise(eOSSLError, "Turning off FIPS mode failed");
}
return enabled;
#else
if (RTEST(enabled))
ossl_raise(eOSSLError, "This version of OpenSSL does not support FIPS mode");
return enabled;
#endif
} Включает или выключает режим FIPS. Включение режима FIPS, очевидно, повлияет только на установки OpenSSL библиотеки, которые поддерживают FIPS. Попытка сделать это в других случаях приведет к ошибке.
Примеры
OpenSSL.fips_mode = true # turn FIPS mode on OpenSSL.fips_mode = false # and off again
Исходный код
static VALUE
ossl_crypto_fixed_length_secure_compare(VALUE dummy, VALUE str1, VALUE str2)
{
const unsigned char *p1 = (const unsigned char *)StringValuePtr(str1);
const unsigned char *p2 = (const unsigned char *)StringValuePtr(str2);
long len1 = RSTRING_LEN(str1);
long len2 = RSTRING_LEN(str2);
if (len1 != len2) {
ossl_raise(rb_eArgError, "inputs must be of equal length");
}
switch (CRYPTO_memcmp(p1, p2, len1)) {
case 0: return Qtrue;
default: return Qfalse;
}
} Сравнение участков памяти с постоянной скоростью для строк фиксированной длины, таких как результаты вычислений HMAC.
Возвращает true если строки идентичны, false если у них одинаковая длина, но они не идентичны. Если длина отличается, генерируется ArgumentError.
Исходный код
# File ext/openssl/lib/openssl.rb, line 33
def self.secure_compare(a, b)
hashed_a = OpenSSL::Digest.digest('SHA256', a)
hashed_b = OpenSSL::Digest.digest('SHA256', b)
OpenSSL.fixed_length_secure_compare(hashed_a, hashed_b) && a == b
end Сравнение участков памяти с постоянной скоростью. Входные данные хешируются с помощью SHA-256 для маскирования длины секрета. Возвращает true если строки идентичны, false в противном случае.
Приватные методы экземпляра
Исходный код
# File ext/openssl/lib/openssl/digest.rb, line 63 def Digest(name) OpenSSL::Digest.const_get(name) end
Возвращает подкласс Digest по имени.
require 'openssl'
OpenSSL::Digest("MD5")
# => OpenSSL::Digest::MD5
Digest("Foo")
# => NameError: wrong constant name Foo
Ruby Core © 1993–2024 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.