模块 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
客户端¶ ↑
一个 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
ruby
OpenSSL
扩展构建时使用的 LibreSSL 版本号(以 16 为基数)。格式为0xMNNFF00f (major minor fix 00 status)
。此常量仅在 LibreSSL 情况下定义。另请参阅手册页
LIBRESSL_VERSION_NUMBER
(3)。- OPENSSL_FIPS
布尔值,指示
OpenSSL
是否支持 FIPS。- OPENSSL_LIBRARY_VERSION
- OPENSSL_VERSION
- 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
公共类方法
根据 name 返回 Digest
子类。
require 'openssl' OpenSSL::Digest("MD5") # => OpenSSL::Digest::MD5 Digest("Foo") # => NameError: wrong constant name Foo
# File ext/openssl/lib/openssl/digest.rb, line 63 def Digest(name) OpenSSL::Digest.const_get(name) end
static VALUE ossl_debug_get(VALUE self) { return dOSSL; }
打开或关闭调试模式。在调试模式下,添加到 OpenSSL
错误队列的所有错误都将打印到 stderr。
static VALUE ossl_debug_set(VALUE self, VALUE val) { dOSSL = RTEST(val) ? Qtrue : Qfalse; return val; }
查看队列中保留的任何剩余错误。
您在此处看到的任何错误可能是由于 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; }
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 模式。打开 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 }
针对固定长度字符串(例如 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; } }
恒定时间内存比较。输入使用 SHA-256 进行哈希处理以掩盖密钥的长度。如果字符串相同,则返回 true
,否则返回 false
。
# 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
私有实例方法
根据 name 返回 Digest
子类。
require 'openssl' OpenSSL::Digest("MD5") # => OpenSSL::Digest::MD5 Digest("Foo") # => NameError: wrong constant name Foo
# File ext/openssl/lib/openssl/digest.rb, line 63 def Digest(name) OpenSSL::Digest.const_get(name) end