Spec-Zone.ru › Ruby 4.0

модуль 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

Сертификаты X509

Создание сертификата

В этом примере с помощью ключа 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 используется OpenSSL::SSL::SSLContext.

context = OpenSSL::SSL::SSLContext.new

Сервер SSL

Для безопасного обмена данными с клиентами серверу SSL необходимы сертификат и закрытый ключ:

context.cert = cert
context.key = key

Затем создайте OpenSSL::SSL::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 и включения шифрования необходимо вызвать OpenSSL::SSL::SSLSocket#connect. Клиентскому сокету не требуются ключ и сертификат.

Обратите внимание: OpenSSL::SSL::SSLSocket#close по умолчанию не закрывает базовый сокет. Если это необходимо, установите OpenSSL::SSL::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. Он может отличаться от версии, используемой во время выполнения.

Эта константа определена только в том случае, если расширение было скомпилировано с LibreSSL. Номер соответствует формату: 0xMNNFF00f (основная версия, дополнительная версия, исправление, 00, статус).

См. также справочную страницу LIBRESSL_VERSION_NUMBER(3).

OPENSSL_FIPS

Логическое значение, указывающее, поддерживает ли библиотека OpenSSL FIPS. Для OpenSSL 3.0 и более поздних версий всегда равно true.

Эта константа устарела и будет удалена в будущем. См. также OpenSSL.fips_mode.

OPENSSL_LIBRARY_VERSION

Строка версии библиотеки OpenSSL, используемой в данный момент во время выполнения.

OPENSSL_VERSION

Строка версии библиотеки OpenSSL, используемой для компиляции расширения Ruby/OpenSSL. Она может отличаться от версии, используемой во время выполнения.

OPENSSL_VERSION_NUMBER

Номер версии библиотеки OpenSSL, используемой для компиляции расширения Ruby/OpenSSL. Он может отличаться от версии, используемой во время выполнения.

Номер версии кодируется одним целочисленным значением. Номер соответствует следующему формату:

OpenSSL 3.0.0 или новее

0xMNN00PP0 (основная версия, дополнительная версия, 00, исправление, 0)

OpenSSL 1.1.1 или старше

0xMNNFFPPS (основная версия, дополнительная версия, исправление, патч, статус)

LibreSSL

0x20000000 (фиксированное значение)

См. также справочную страницу OPENSSL_VERSION_NUMBER(3).

VERSION

Строка версии Ruby/OpenSSL.

Публичные методы класса

Digest (name) Показать исходный код
# 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

OpenSSL::Digest("Foo")
# => NameError: wrong constant name Foo
debug → true | false Показать исходный код
static VALUE
ossl_debug_get(VALUE self)
{
    return dOSSL;
}

Возвращает признак того, включён ли в данный момент режим отладки Ruby/OpenSSL.

debug = boolean Показать исходный код
static VALUE
ossl_debug_set(VALUE self, VALUE val)
{
    dOSSL = RTEST(val) ? Qtrue : Qfalse;

    return val;
}

Включает или отключает режим отладки. В режиме отладки все ошибки, добавленные в очередь ошибок OpenSSL, выводятся в stderr.

errors → [String...] Показать исходный код
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/OpenSSL. Если здесь обнаружатся ошибки, это, вероятно, указывает на ошибку в расширении. Сообщите о проблеме на github.com/ruby/openssl.

Для отладки вашей программы может пригодиться OpenSSL.debug=.

fips_mode → true | false Показать исходный код
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) || defined(OPENSSL_IS_AWSLC)
    VALUE enabled;
    enabled = FIPS_mode() ? Qtrue : Qfalse;
    return enabled;
#else
    return Qfalse;
#endif
}

Возвращает признак того, включён ли в данный момент режим FIPS.

fips_mode = boolean Показать исходный код
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) || defined(OPENSSL_IS_AWSLC)
    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
fixed_length_secure_compare(string, string) → true or false Показать исходный код
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.

secure_compare(string, string) → true or false Показать исходный код
# File ext/openssl/lib/openssl.rb, line 36
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 в противном случае.

Этот метод затратен из-за вычисления хеша SHA-256. В большинстве случаев, когда длины входных данных заведомо равны или не являются конфиденциальными, вместо него следует использовать OpenSSL.fixed_length_secure_compare.

Приватные методы экземпляра

Digest (name) Показать исходный код
# 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

OpenSSL::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.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API