Overview
The OpenSSL library in Ruby exposes cryptographic operations through several key classes. The OpenSSL::Cipher
class handles symmetric encryption and decryption, while OpenSSL::PKey
manages asymmetric cryptography including RSA, DSA, and EC keys. Certificate operations use OpenSSL::X509::Certificate
and related classes, with OpenSSL::SSL
providing secure socket functionality.
Ruby's OpenSSL implementation maintains the same security model as the underlying C library. All operations require explicit algorithm selection, key management, and parameter configuration. The library does not provide default security settings, requiring developers to make informed choices about cryptographic parameters.
require 'openssl'
# Generate RSA key pair
key = OpenSSL::PKey::RSA.generate(2048)
# => #<OpenSSL::PKey::RSA:0x...>
# Create AES cipher
cipher = OpenSSL::Cipher.new('AES-256-GCM')
# => #<OpenSSL::Cipher:0x...>
# Generate random bytes
random_data = OpenSSL::Random.random_bytes(32)
# => "\x9A\x7B\x2C..." (32 bytes of random data)
The module organizes functionality into logical namespaces. Digest operations use OpenSSL::Digest
classes, HMAC operations use OpenSSL::HMAC
, and key derivation functions reside in OpenSSL::KDF
. Each namespace provides both class-level convenience methods and instance-based workflows.
Certificate management involves multiple interconnected classes. OpenSSL::X509::Certificate
represents X.509 certificates, OpenSSL::X509::Request
handles certificate signing requests, and OpenSSL::X509::Store
manages certificate validation chains. These classes integrate with OpenSSL::PKey
for key operations and OpenSSL::ASN1
for low-level structure manipulation.
Basic Usage
Symmetric encryption with OpenSSL::Cipher
requires algorithm selection, key derivation, and initialization vector management. The cipher object maintains state through the encryption process, requiring proper initialization before use.
require 'openssl'
# Encrypt data with AES-256-GCM
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
# Generate key and IV
key = cipher.random_key
iv = cipher.random_iv
# Set key and IV
cipher.key = key
cipher.iv = iv
plaintext = "Sensitive data to encrypt"
ciphertext = cipher.update(plaintext) + cipher.final
auth_tag = cipher.auth_tag
# Decrypt the data
decipher = OpenSSL::Cipher.new('AES-256-GCM')
decipher.decrypt
decipher.key = key
decipher.iv = iv
decipher.auth_tag = auth_tag
decrypted = decipher.update(ciphertext) + decipher.final
# => "Sensitive data to encrypt"
Asymmetric cryptography centers on key generation and management. RSA keys support both encryption and signing operations, while EC keys provide efficient alternatives for signing and key exchange.
# Generate RSA key pair
rsa_key = OpenSSL::PKey::RSA.generate(2048)
public_key = rsa_key.public_key
# Encrypt with public key
encrypted = public_key.public_encrypt("secret message")
# Decrypt with private key
decrypted = rsa_key.private_decrypt(encrypted)
# => "secret message"
# Sign data with private key
signature = rsa_key.sign('SHA256', "data to sign")
# Verify signature with public key
verified = public_key.verify('SHA256', signature, "data to sign")
# => true
Message digests and HMAC operations provide data integrity verification. The OpenSSL::Digest
class supports various hashing algorithms, while OpenSSL::HMAC
adds keyed authentication.
# Create message digest
digest = OpenSSL::Digest.new('SHA256')
hash = digest.digest("data to hash")
# => 32-byte binary hash
# Hexadecimal representation
hex_hash = digest.hexdigest("data to hash")
# => "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"
# HMAC with secret key
hmac = OpenSSL::HMAC.digest('SHA256', 'secret_key', 'message')
# => 32-byte authentication code
# Hexadecimal HMAC
hex_hmac = OpenSSL::HMAC.hexdigest('SHA256', 'secret_key', 'message')
# => "hexadecimal representation"
Certificate creation involves multiple steps including key generation, certificate signing request creation, and certificate issuance. Each step requires specific parameter configuration.
# Create certificate signing request
key = OpenSSL::PKey::RSA.generate(2048)
csr = OpenSSL::X509::Request.new
# Set subject distinguished name
csr.subject = OpenSSL::X509::Name.parse("/CN=example.com/O=Example Corp/C=US")
csr.public_key = key.public_key
# Sign the CSR
csr.sign(key, OpenSSL::Digest.new('SHA256'))
# Create self-signed certificate
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = 1
cert.not_before = Time.now
cert.not_after = Time.now + (365 * 24 * 60 * 60) # 1 year
cert.public_key = key.public_key
cert.subject = csr.subject
cert.issuer = cert.subject # self-signed
# Sign certificate
cert.sign(key, OpenSSL::Digest.new('SHA256'))
Error Handling & Debugging
OpenSSL operations generate specific exception types that indicate different failure modes. The OpenSSL::OpenSSLError
serves as the base exception class, with subclasses providing specific error context.
Cipher operations fail when keys, initialization vectors, or authentication tags are incorrect. Padding errors indicate data corruption or wrong keys, while authentication failures in AEAD modes suggest tampering or incorrect parameters.
def safe_decrypt(ciphertext, key, iv, auth_tag)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.decrypt
cipher.key = key
cipher.iv = iv
cipher.auth_tag = auth_tag
cipher.update(ciphertext) + cipher.final
rescue OpenSSL::Cipher::CipherError => e
case e.message
when /bad decrypt/
raise DecryptionError, "Invalid key or corrupted data"
when /wrong tag/
raise AuthenticationError, "Authentication tag verification failed"
else
raise CryptographicError, "Cipher operation failed: #{e.message}"
end
end
Key operations produce OpenSSL::PKey::PKeyError
exceptions when parameters are invalid or operations are unsupported. RSA operations fail with specific errors for padding issues, key size mismatches, or unsupported operations.
def safe_rsa_encrypt(data, public_key, max_retries: 3)
retries = 0
begin
public_key.public_encrypt(data, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
rescue OpenSSL::PKey::RSAError => e
case e.message
when /data too large for key size/
raise DataTooLargeError, "Data exceeds RSA key capacity"
when /unknown padding type/
raise PaddingError, "Invalid padding mode specified"
else
retries += 1
if retries <= max_retries
sleep(0.1 * retries)
retry
else
raise EncryptionError, "RSA encryption failed after #{max_retries} retries: #{e.message}"
end
end
end
end
Certificate validation produces OpenSSL::X509::CertificateError
and OpenSSL::X509::StoreError
exceptions. Chain validation failures, expired certificates, and revocation status errors require different handling approaches.
def validate_certificate_chain(cert, intermediates, root_store)
store = OpenSSL::X509::Store.new
store.add_cert(root_store)
intermediates.each { |intermediate| store.add_cert(intermediate) }
store_context = OpenSSL::X509::StoreContext.new(store, cert)
unless store_context.verify
error_code = store_context.error
case error_code
when OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED
raise CertificateExpiredError, "Certificate expired: #{cert.subject}"
when OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT
raise MissingIssuerError, "Cannot find issuer certificate"
when OpenSSL::X509::V_ERR_CERT_CHAIN_TOO_LONG
raise ChainTooLongError, "Certificate chain exceeds maximum length"
else
raise CertificateValidationError, "Validation failed: #{store_context.error_string}"
end
end
true
rescue OpenSSL::X509::StoreError => e
raise CertificateStoreError, "Store operation failed: #{e.message}"
end
Debugging cryptographic operations requires careful logging without exposing sensitive data. Key fingerprints, certificate serial numbers, and algorithm parameters provide debugging information without compromising security.
class CryptoDebugger
def self.debug_cipher_operation(cipher, operation)
{
algorithm: cipher.name,
operation: operation,
key_length: cipher.key_len,
iv_length: cipher.iv_len,
auth_data_length: cipher.auth_data_length,
timestamp: Time.now.iso8601
}
end
def self.debug_certificate(cert)
{
subject: cert.subject.to_s,
issuer: cert.issuer.to_s,
serial: cert.serial.to_s,
not_before: cert.not_before.iso8601,
not_after: cert.not_after.iso8601,
public_key_algorithm: cert.public_key.class.name,
fingerprint: Digest::SHA256.hexdigest(cert.to_der)
}
end
end
Production Patterns
SSL/TLS client configuration requires careful parameter selection for security and compatibility. The OpenSSL::SSL::SSLContext
class manages cipher suites, protocol versions, and certificate verification settings.
class SecureHttpClient
def self.create_ssl_context
context = OpenSSL::SSL::SSLContext.new
# Set minimum TLS version
context.min_version = OpenSSL::SSL::TLS1_2_VERSION
context.max_version = OpenSSL::SSL::TLS1_3_VERSION
# Configure cipher suites
context.ciphers = [
'TLS_AES_256_GCM_SHA384',
'TLS_CHACHA20_POLY1305_SHA256',
'TLS_AES_128_GCM_SHA256',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-RSA-CHACHA20-POLY1305'
]
# Enable certificate verification
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
context.verify_hostname = true
# Load system certificate store
context.cert_store = OpenSSL::X509::Store.new
context.cert_store.set_default_paths
context
end
def self.make_request(uri, ca_file: nil)
socket = TCPSocket.new(uri.host, uri.port || 443)
ssl_context = create_ssl_context
ssl_context.ca_file = ca_file if ca_file
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
ssl_socket.hostname = uri.host
ssl_socket.connect
# Verify connection security
verify_connection_security(ssl_socket)
ssl_socket
ensure
ssl_socket&.close
socket&.close
end
private
def self.verify_connection_security(ssl_socket)
cipher = ssl_socket.cipher
raise InsecureConnectionError, "Weak cipher: #{cipher[0]}" if weak_cipher?(cipher[0])
cert = ssl_socket.peer_cert
raise CertificateError, "Certificate expires soon" if cert.not_after < Time.now + (7 * 24 * 60 * 60)
end
def self.weak_cipher?(cipher_name)
cipher_name.match?(/RC4|DES|MD5|NULL/)
end
end
Data encryption for storage requires key derivation, secure random generation, and authenticated encryption. Production systems need key rotation capabilities and secure key storage integration.
class DataEncryption
ALGORITHM = 'AES-256-GCM'.freeze
KEY_DERIVATION_ITERATIONS = 100_000
def initialize(master_key)
@master_key = master_key
end
def encrypt(plaintext, additional_data: nil)
salt = OpenSSL::Random.random_bytes(32)
key = derive_key(@master_key, salt)
cipher = OpenSSL::Cipher.new(ALGORITHM)
cipher.encrypt
cipher.key = key
iv = cipher.random_iv
cipher.auth_data = additional_data if additional_data
ciphertext = cipher.update(plaintext) + cipher.final
auth_tag = cipher.auth_tag
# Combine all components
[
salt,
iv,
auth_tag,
ciphertext
].map { |component| Base64.strict_encode64(component) }.join('.')
end
def decrypt(encrypted_data, additional_data: nil)
components = encrypted_data.split('.')
raise ArgumentError, "Invalid encrypted data format" unless components.length == 4
salt, iv, auth_tag, ciphertext = components.map { |c| Base64.strict_decode64(c) }
key = derive_key(@master_key, salt)
cipher = OpenSSL::Cipher.new(ALGORITHM)
cipher.decrypt
cipher.key = key
cipher.iv = iv
cipher.auth_tag = auth_tag
cipher.auth_data = additional_data if additional_data
cipher.update(ciphertext) + cipher.final
end
private
def derive_key(master_key, salt)
OpenSSL::KDF.pbkdf2_hmac(
master_key,
salt: salt,
iterations: KEY_DERIVATION_ITERATIONS,
length: 32,
hash: OpenSSL::Digest.new('SHA256')
)
end
end
Certificate management in production requires automated renewal, validation monitoring, and secure storage. Integration with certificate authorities and ACME protocol implementations handles certificate lifecycle management.
class CertificateManager
def initialize(key_store:, cert_store:, ca_config:)
@key_store = key_store
@cert_store = cert_store
@ca_config = ca_config
end
def check_certificate_expiry(domain)
cert = @cert_store.get_certificate(domain)
return { status: :missing, domain: domain } unless cert
days_until_expiry = (cert.not_after - Time.now) / (24 * 60 * 60)
if days_until_expiry <= 7
{ status: :expired_soon, domain: domain, days: days_until_expiry.to_i }
elsif days_until_expiry <= 30
{ status: :expiring, domain: domain, days: days_until_expiry.to_i }
else
{ status: :valid, domain: domain, days: days_until_expiry.to_i }
end
end
def renew_certificate(domain)
# Generate new key pair
new_key = OpenSSL::PKey::RSA.generate(2048)
# Create certificate signing request
csr = create_csr(domain, new_key)
# Submit to certificate authority
new_cert = submit_csr_to_ca(csr)
# Validate new certificate
validate_new_certificate(new_cert, new_key, domain)
# Store new key and certificate
@key_store.store_key(domain, new_key)
@cert_store.store_certificate(domain, new_cert)
{ status: :renewed, domain: domain, expires_at: new_cert.not_after }
rescue StandardError => e
{ status: :failed, domain: domain, error: e.message }
end
private
def create_csr(domain, key)
csr = OpenSSL::X509::Request.new
csr.subject = OpenSSL::X509::Name.parse("/CN=#{domain}")
csr.public_key = key.public_key
# Add Subject Alternative Name extension
extension_factory = OpenSSL::X509::ExtensionFactory.new
san_extension = extension_factory.create_extension(
'subjectAltName',
"DNS:#{domain},DNS:*.#{domain}"
)
csr.add_attribute(
OpenSSL::X509::Attribute.new(
'extReq',
OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence([san_extension])])
)
)
csr.sign(key, OpenSSL::Digest.new('SHA256'))
csr
end
def validate_new_certificate(cert, key, expected_domain)
# Verify certificate matches private key
test_data = 'certificate validation test'
signature = key.sign('SHA256', test_data)
unless cert.public_key.verify('SHA256', signature, test_data)
raise CertificateValidationError, "Certificate does not match private key"
end
# Verify domain in certificate
cert_domains = extract_domains_from_certificate(cert)
unless cert_domains.include?(expected_domain)
raise CertificateValidationError, "Certificate does not include expected domain: #{expected_domain}"
end
end
end
Common Pitfalls
Initialization vector reuse in symmetric encryption compromises security completely. Each encryption operation requires a unique IV, even when using the same key. Counter modes and GCM modes are particularly vulnerable to IV reuse attacks.
# WRONG: Reusing IV
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
cipher.key = shared_key
cipher.iv = fixed_iv # Never do this!
# CORRECT: Generate unique IV for each operation
def encrypt_with_unique_iv(plaintext, key)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
cipher.key = key
iv = cipher.random_iv # Always generate new IV
ciphertext = cipher.update(plaintext) + cipher.final
auth_tag = cipher.auth_tag
# Return IV with ciphertext for decryption
{ iv: iv, ciphertext: ciphertext, auth_tag: auth_tag }
end
Key derivation without salt allows rainbow table attacks against password-derived keys. The salt must be cryptographically random and stored with the derived key material.
# WRONG: No salt in key derivation
derived_key = OpenSSL::KDF.pbkdf2_hmac(password, iterations: 100_000, length: 32)
# CORRECT: Always use random salt
def derive_key_safely(password, stored_salt = nil)
salt = stored_salt || OpenSSL::Random.random_bytes(32)
derived_key = OpenSSL::KDF.pbkdf2_hmac(
password,
salt: salt,
iterations: 100_000,
length: 32,
hash: OpenSSL::Digest.new('SHA256')
)
{ key: derived_key, salt: salt }
end
Certificate hostname verification bypasses create security vulnerabilities. Disabling hostname verification allows man-in-the-middle attacks even with valid certificates.
# WRONG: Disabling certificate verification
context = OpenSSL::SSL::SSLContext.new
context.verify_mode = OpenSSL::SSL::VERIFY_NONE # Never do this in production!
# CORRECT: Proper certificate verification
def create_secure_context(trusted_ca_file = nil)
context = OpenSSL::SSL::SSLContext.new
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
context.verify_hostname = true
# Use system certificate store or custom CA file
context.cert_store = OpenSSL::X509::Store.new
if trusted_ca_file
context.ca_file = trusted_ca_file
else
context.cert_store.set_default_paths
end
context
end
Signature verification timing attacks occur when verification functions return early on mismatched signatures. Constant-time comparison prevents information leakage about the expected signature.
# WRONG: Early return allows timing attacks
def verify_signature_insecure(public_key, signature, data)
expected_signature = public_key.sign('SHA256', data)
signature == expected_signature # Timing attack vulnerability
end
# CORRECT: Constant-time comparison
def verify_signature_secure(public_key, signature, data)
begin
public_key.verify('SHA256', signature, data)
rescue OpenSSL::PKey::PKeyError
false
end
end
# For HMAC verification, use secure comparison
def verify_hmac_secure(secret, message, provided_hmac)
expected_hmac = OpenSSL::HMAC.digest('SHA256', secret, message)
# Constant-time comparison prevents timing attacks
return false unless provided_hmac.bytesize == expected_hmac.bytesize
result = 0
provided_hmac.bytes.zip(expected_hmac.bytes) do |a, b|
result |= a ^ b
end
result.zero?
end
Random number generation using predictable sources compromises cryptographic security. Ruby's rand
function is not cryptographically secure and should never be used for keys, IVs, or nonces.
# WRONG: Using predictable random numbers
iv = Array.new(16) { rand(256) }.pack('C*') # Predictable!
key = (0..31).map { rand(256) }.pack('C*') # Predictable!
# CORRECT: Use cryptographically secure random number generation
def generate_secure_key(length = 32)
OpenSSL::Random.random_bytes(length)
end
def generate_secure_iv(cipher)
cipher.random_iv
end
# Seed additional entropy if needed (usually not required)
OpenSSL::Random.load_random_file('/dev/random')
Exception handling that exposes cryptographic details aids attackers in distinguishing between different failure modes. Generic error messages prevent information leakage about internal cryptographic state.
# WRONG: Exposing cryptographic details in exceptions
def decrypt_user_data(encrypted_data, user_password)
# ... decryption logic ...
rescue OpenSSL::Cipher::CipherError => e
raise "Decryption failed: #{e.message}" # Exposes internal details
end
# CORRECT: Generic error messages
def decrypt_user_data_secure(encrypted_data, user_password)
# ... decryption logic ...
rescue OpenSSL::Cipher::CipherError, OpenSSL::OpenSSLError
raise AuthenticationError, "Authentication failed" # Generic message
end
Reference
Core Classes
Class | Purpose | Key Methods |
---|---|---|
OpenSSL::Cipher |
Symmetric encryption/decryption | #encrypt , #decrypt , #update , #final |
OpenSSL::PKey::RSA |
RSA public-key cryptography | #public_encrypt , #private_decrypt , #sign , #verify |
OpenSSL::PKey::EC |
Elliptic curve cryptography | #dh_compute_key , #sign , #verify |
OpenSSL::Digest |
Message digests | #digest , #hexdigest , #update |
OpenSSL::HMAC |
Hash-based message authentication | .digest , .hexdigest |
OpenSSL::X509::Certificate |
X.509 certificates | #verify , #check_private_key |
OpenSSL::SSL::SSLContext |
SSL/TLS context configuration | #ciphers= , #verify_mode= |
Cipher Algorithms
Algorithm | Key Size (bits) | Block Size | Mode | Security Level |
---|---|---|---|---|
AES-128-GCM |
128 | 128 | GCM | High |
AES-256-GCM |
256 | 128 | GCM | High |
ChaCha20-Poly1305 |
256 | Stream | AEAD | High |
AES-128-CBC |
128 | 128 | CBC | Medium |
AES-256-CBC |
256 | 128 | CBC | Medium |
Key Generation Methods
Method | Parameters | Returns | Usage |
---|---|---|---|
OpenSSL::PKey::RSA.generate(size) |
size (Integer) |
OpenSSL::PKey::RSA |
RSA key pair generation |
OpenSSL::PKey::EC.generate(curve) |
curve (String/Group) |
OpenSSL::PKey::EC |
EC key pair generation |
OpenSSL::Random.random_bytes(length) |
length (Integer) |
String |
Cryptographically secure random data |
cipher.random_key |
None | String |
Random key for cipher |
cipher.random_iv |
None | String |
Random IV for cipher |
Digest Algorithms
Algorithm | Output Size (bits) | Security Level | Usage |
---|---|---|---|
SHA256 |
256 | High | General purpose |
SHA384 |
384 | High | High security applications |
SHA512 |
512 | High | High security applications |
SHA3-256 |
256 | High | Alternative to SHA-2 |
BLAKE2b |
Variable | High | Fast hashing |
SSL/TLS Configuration Options
Option | Values | Description |
---|---|---|
verify_mode |
VERIFY_NONE , VERIFY_PEER |
Certificate verification mode |
min_version |
TLS1_2_VERSION , TLS1_3_VERSION |
Minimum TLS protocol version |
max_version |
TLS1_2_VERSION , TLS1_3_VERSION |
Maximum TLS protocol version |
ciphers |
Array of cipher names | Allowed cipher suites |
verify_hostname |
true , false |
Enable hostname verification |
Exception Hierarchy
Exception | Parent Class | Raised When |
---|---|---|
OpenSSL::OpenSSLError |
StandardError |
Base OpenSSL exception |
OpenSSL::Cipher::CipherError |
OpenSSL::OpenSSLError |
Cipher operation failure |
OpenSSL::PKey::PKeyError |
OpenSSL::OpenSSLError |
Public key operation failure |
OpenSSL::X509::CertificateError |
OpenSSL::OpenSSLError |
Certificate operation failure |
OpenSSL::SSL::SSLError |
OpenSSL::OpenSSLError |
SSL/TLS operation failure |
Key Derivation Functions
Function | Parameters | Output | Use Case |
---|---|---|---|
OpenSSL::KDF.pbkdf2_hmac |
password, salt, iterations, length, hash | Derived key | Password-based key derivation |
OpenSSL::KDF.scrypt |
password, salt, N, r, p, length | Derived key | Memory-hard key derivation |
OpenSSL::KDF.hkdf |
ikm, salt, info, length, hash | Derived key | Key expansion |
Certificate Extensions
Extension OID | Name | Purpose |
---|---|---|
2.5.29.17 |
Subject Alternative Name | Additional hostnames/IPs |
2.5.29.15 |
Key Usage | Allowed key operations |
2.5.29.37 |
Extended Key Usage | Specific key usage purposes |
2.5.29.19 |
Basic Constraints | CA certificate constraints |
2.5.29.14 |
Subject Key Identifier | Key identifier |