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 的簽章。
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 預設不會關閉底層套接字。如果需要,請將 Set
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(主要次要修正 00 狀態)
。此常數僅在 LibreSSL 案例中定義。另請參閱手冊頁面
LIBRESSL_VERSION_NUMBER
(3)。- OPENSSL_FIPS
布林值,表示
OpenSSL
是否具有 FIPS 功能- OPENSSL_LIBRARY_VERSION
- OPENSSL_VERSION
- OPENSSL_VERSION_NUMBER
用於建置 Ruby
OpenSSL
擴充套件的OpenSSL
版本號碼(16 進位)。格式如下。OpenSSL
3-
0xMNN00PP0(主要次要 00 修補程式 0)
OpenSSL
3 之前的版本-
0xMNNFFPPS(主要次要修正修補程式狀態)
- 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 模式顯然只會對 OpenSSL
函式庫的 FIPS 可用安裝產生影響。否則嘗試執行此操作會導致錯誤。
範例¶ ↑
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