Spec-Zone.ru › Ruby 3.3

модуль 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, с которой был построен плагин ruby OpenSSL.

OPENSSL_VERSION_NUMBER

Номер версии OpenSSL, с которой был построен плагин ruby OpenSSL (в шестнадцатеричном формате). Форматы ниже.

OpenSSL 3

0xMNN00PP0 (major minor 00 patch 0)

OpenSSL до 3

0xMNNFFPPS (major minor fix patch status)

LibreSSL

0x20000000 (fixed value)

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

VERSION

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

Digest(name) Показать исходный код
# 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
debug → true | false Показать исходный код
static VALUE
ossl_debug_get(VALUE self)
{
    return dOSSL;
}
debug = boolean → boolean Показать исходный код
static VALUE
ossl_debug_set(VALUE self, VALUE val)
{
    dOSSL = RTEST(val) ? Qtrue : Qfalse;

    return val;
}

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

errors → [String...] Показать исходный код
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;
}

Просмотр оставшихся ошибок в очереди.

Любые ошибки, которые вы видите здесь, вероятно, связаны с ошибкой в реализации Ruby OpenSSL.

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)
    VALUE enabled;
    enabled = FIPS_mode() ? Qtrue : Qfalse;
    return enabled;
#else
    return Qfalse;
#endif
}
fips_mode = boolean → 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)
    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) → boolean Показать исходный код
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) → boolean Показать исходный код
# File ext/openssl/lib/openssl.rb, line 32
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 в противном случае.

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

Digest(name) Показать исходный код
# 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–2022 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.

Spec-Zone.ru

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