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

用於建置 Ruby OpenSSL 擴充套件的 OpenSSL 版本

OPENSSL_VERSION_NUMBER

用於建置 Ruby OpenSSL 擴充套件的 OpenSSL 版本號碼(16 進位)。格式如下。

OpenSSL 3

0xMNN00PP0(主要次要 00 修補程式 0)

OpenSSL 3 之前的版本

0xMNNFFPPS(主要次要修正修補程式狀態)

LibreSSL

0x20000000(固定值)

另請參閱手冊頁面 OPENSSL_VERSION_NUMBER(3)。

VERSION

公開類別方法

Digest(name) 按一下以切換來源

根據 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
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 模式顯然只會對 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
}
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 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

私有執行個體方法

Digest(name) 按一下以切換來源

根據 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