模块 OpenSSL

OpenSSL 提供 SSL、TLS 和通用目的加密。它封装了 OpenSSL 库。

示例

所有示例都假设您已使用以下命令加载了 OpenSSL

require 'openssl'

这些示例相互构建。例如,在下一个示例中创建的密钥将在这些示例中一直使用。

密钥

创建密钥

此示例创建一个 2048 位 RSA 密钥对,并将其写入当前目录。

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,然后使用 PBKDF2 从密码生成一个随机 IV 和一个密钥。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')

支持的扩展列表(在某些情况下还有其可能的值)可以从 OpenSSL 源代码中的“objects.h”文件推导出。

签名证书

要签名证书,请设置颁发者并使用 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。

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 连接。SSLContext 用于设置 SSL 会话。

context = OpenSSL::SSL::SSLContext.new

SSL 服务器

SSL 服务器需要证书和私钥才能与其客户端安全通信

context.cert = cert
context.key = key

然后使用 TCP 服务器套接字和上下文创建 SSLServer。像普通 TCP 服务器一样使用 SSLServer。

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 客户端

使用 TCP 套接字和上下文创建 SSL 客户端。必须调用 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

Ruby OpenSSL 扩展构建时使用的 LibreSSL 版本号(以 16 为底)。格式为 0xMNNFF00f (major minor fix 00 status)。此常量仅在 LibreSSL 情况下定义。

另请参阅手册页 LIBRESSL_VERSION_NUMBER(3)。

OPENSSL_FIPS

布尔值,指示 OpenSSL 是否支持 FIPS。

OPENSSL_LIBRARY_VERSION
OPENSSL_VERSION

Ruby OpenSSL 扩展构建时使用的 OpenSSL 版本

OPENSSL_VERSION_NUMBER

Ruby OpenSSL 扩展构建时使用的 OpenSSL 版本号(以 16 为底)。格式如下。

OpenSSL 3

0xMNN00PP0 (major minor 00 patch 0)

OpenSSL 版本 3 之前

0xMNNFFPPS (major minor fix patch status)

LibreSSL

0x20000000 (固定值)

另请参阅手册页 OPENSSL_VERSION_NUMBER(3)。

VERSION

公共类方法

Digest(name) click to toggle source

根据 name 返回 Digest 子类

require 'openssl'

OpenSSL::Digest("MD5")
# => OpenSSL::Digest::MD5

Digest("Foo")
# => NameError: wrong constant name Foo
# File openssl/lib/openssl/digest.rb, line 63
def Digest(name)
  OpenSSL::Digest.const_get(name)
end
debug → true | false 点击切换源代码
static VALUE
ossl_debug_get(VALUE self)
{
    return dOSSL;
}
debug = boolean → boolean 点击切换源代码

开启或关闭调试模式。在调试模式下,所有添加到 OpenSSL 错误队列的错误将被打印到 stderr。

static VALUE
ossl_debug_set(VALUE self, VALUE val)
{
    dOSSL = RTEST(val) ? Qtrue : Qfalse;

    return val;
}
errors → [String...] 点击切换源代码

查看队列中剩余的任何错误。

您在此处看到的任何错误可能是由于 Ruby 的 OpenSSL 实现中的错误造成的。

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;
}
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 点击切换源代码

开启或关闭 FIPS 模式。开启 FIPS 模式显然只对支持 FIPS 的 OpenSSL 库安装有效。否则尝试这样做会导致错误。

示例

OpenSSL.fips_mode = true   # turn FIPS mode on
OpenSSL.fips_mode = false  # and off again
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
}
fixed_length_secure_compare(string, string) → boolean 点击切换源代码

针对固定长度字符串(例如 HMAC 计算结果)的恒定时间内存比较。

如果字符串相同,则返回 true;如果字符串长度相同但内容不同,则返回 false。如果长度不同,则会引发 ArgumentError

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;
    }
}
secure_compare(string, string) → boolean 点击切换源代码

恒定时间内存比较。输入使用 SHA-256 进行哈希处理以掩盖密钥的长度。如果字符串相同,则返回 true;否则返回 false

# File 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

私有实例方法

Digest(name) click to toggle source

根据 name 返回 Digest 子类

require 'openssl'

OpenSSL::Digest("MD5")
# => OpenSSL::Digest::MD5

Digest("Foo")
# => NameError: wrong constant name Foo
# File openssl/lib/openssl/digest.rb, line 63
def Digest(name)
  OpenSSL::Digest.const_get(name)
end