модуль 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) — это доверенная третья сторона, которая позволяет проверять принадлежность неизвестных сертификатов. CA подписывает ключи, подтверждая доверие к их владельцам. Получивший ключ пользователь может проверить подпись с помощью открытого ключа CA.
Ключ CA
Ключи CA представляют большую ценность, поэтому мы шифруем их, сохраняем на диске и следим за тем, чтобы другие пользователи не могли их читать.
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
Сертификат CA создаётся так же, как описанный выше сертификат, но с другими расширениями.
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 можно использовать в качестве ключа центра сертификации.
ca_cert.add_extension \
extension_factory.create_extension('basicConstraints', 'CA:TRUE', true)
Это расширение указывает, что ключ CA можно использовать для проверки подписей как сертификатов, так и отзывов сертификатов.
ca_cert.add_extension \
extension_factory.create_extension(
'keyUsage', 'cRLSign,keyCertSign', true)
Корневые сертификаты CA являются самоподписанными.
ca_cert.sign ca_key, OpenSSL::Digest.new('SHA1')
Сертификат CA сохраняется на диске, чтобы его можно было распространить среди всех пользователей ключей, которые будет подписывать этот CA.
open 'ca_cert.pem', 'w' do |io| io.write ca_cert.to_pem end
Запрос на подпись сертификата
CA подписывает ключи на основании запроса на подпись сертификата (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 сохраняется на диск и отправляется в CA для подписания.
open 'csr.pem', 'w' do |io| io.write csr.to_pem end
Создание сертификата из CSR
Получив CSR, CA проверяет его перед подписанием. Минимальная проверка может заключаться в проверке подписи CSR.
csr = OpenSSL::X509::Request.new File.read 'csr.pem' raise 'CSR can not be verified' unless csr.verify csr.public_key
После проверки создаётся сертификат, которому назначаются различные области применения; он подписывается ключом CA и возвращается отправителю запроса.
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 и контекста. Чтобы начать рукопожатие SSL и включить шифрование, необходимо вызвать SSLSocket#connect. Для клиентского сокета ключ и сертификат не требуются.
Обратите внимание: по умолчанию 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, с которой было собрано расширение RubyOpenSSL - 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, соответствующий name
require 'openssl'
OpenSSL::Digest("MD5")
# => OpenSSL::Digest::MD5
Digest("Foo")
# => NameError: wrong constant name Foo
static VALUE
ossl_debug_get(VALUE self)
{
return dOSSL;
} 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, соответствующий name
require 'openssl'
OpenSSL::Digest("MD5")
# => OpenSSL::Digest::MD5
Digest("Foo")
# => NameError: wrong constant name Foo
Ruby Core © 1993–2025 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.