Bleichenbacher Attack in Rails with Mutual Tls
Bleichenbacher Attack in Rails with Mutual Tls — how this specific combination creates or exposes the vulnerability
A Bleichenbacher attack targets RSA encryption schemes that use PKCS#1 v1.5 padding and rely on an oracle that distinguishes between padding errors and other errors. In Rails, this historically appeared when applications decrypted ciphertext (for example, from an API client or a JWT) and returned different HTTP statuses or timing differences for bad padding versus other failures. An attacker can automate adaptive chosen-ciphertext queries to gradually reveal the plaintext without the private key.
Mutual TLS (mTLS) adds client certificate authentication on top of server authentication. While mTLS strengthens identity verification, it does not change the cryptographic behavior of server-side decryption. If a Rails endpoint accepts client certificates and then processes RSA-encrypted data using a vulnerable PKCS#1 v1.5 decryptor, the mTLS channel may still allow an authenticated client to act as the oracle. In practice, this means an attacker that possesses a valid client certificate (or one obtained via weak issuance or misconfiguration) can repeatedly send crafted ciphertexts to the same endpoint and observe timing differences or error-message variation to perform the Bleichenbacher adaptive attack. The presence of mTLS therefore shifts the threat model: the attacker must first obtain a client cert, but once that condition is met, the padding oracle remains exploitable if the server’s decryption logic is not hardened.
In Rails, common scenarios include:
- API endpoints that decrypt an encrypted field (e.g., encrypted_payload) using OpenSSL::PKey::RSA with default padding, where errors are surfaced as 500 responses or detailed messages that differ for padding failures.
- Legacy integrations that expect JWTs or tokens encrypted with RSA+PKCS#1 v1.5 and perform decrypt-then-verify or verify-then-decrypt patterns without constant-time checks.
Without mitigations, an attacker with a valid client certificate (or via a compromised certificate issued by a lenient CA) can recover session keys or other sensitive data by issuing many requests and observing subtle timing differences or error responses across the mTLS-secured connection.
Mutual Tls-Specific Remediation in Rails — concrete code fixes
Remediation focuses on ensuring that decryption does not leak distinguishability via errors or timing, and that mTLS is correctly configured. Below are concrete practices and code examples.
1. Use constant-time decryption and avoid padding-oracle responses
Do not branch on padding validity in a way that changes timing or status codes. Instead, ensure decryption either always succeeds with a valid key or fails with a uniform exception and a generic error response.
# config/initializers/constant_time_rsa_decrypt.rb
module ConstantTimeRSA
def self.decrypt_base64(ciphertext_base64, private_key_pem)
ciphertext = Base64.strict_decode64(ciphertext_base64)
private_key = OpenSSL::PKey::RSA.new(private_key_pem)
# Use OAEP where possible; if you must use PKCS1 v1.5, process uniformly.
begin
# Perform decrypt and then validate in a way that does not early-exit on padding.
decrypted = private_key.private_decrypt(ciphertext, OpenSSL::PKey::RSA::NO_PADDING)
# Masking step: always do a dummy operation to reduce timing variance.
dummy_key = OpenSSL::PKey::RSA.new(2048)
dummy_cipher = "\x00" * 256
begin
dummy_key.private_decrypt(dummy_cipher, OpenSSL::PKey::RSA::NO_PADDING) rescue nil
end
decrypted
rescue => e
# Log the exception internally, but return a generic error to the caller.
Rails.logger.error("RSA decryption failed: #{e.class}")
raise Errors::SecurityError.new("Decryption error")
end
end
end
2. Enforce modern padding and encryption practices
Prefer RSA-OAEP over PKCS#1 v1.5. If you interoperate with external clients, negotiate algorithm choices via API policy rather than accepting legacy modes.
# Example: Enforce OAEP when decrypting in a service object
class DecryptSensitiveService
OAEP_DIGEST = OpenSSL::Digest::SHA256.new
def initialize(ciphertext_base64, private_key_pem)
@ciphertext = Base64.strict_decode64(ciphertext_base64)
@private_key = OpenSSL::PKey::RSA.new(private_key_pem)
end
def call
@private_key.private_decrypt(@ciphertext, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
rescue OpenSSL::PKey::RSAError => e
Rails.logger.warn("RSA OAEP decryption failed")
raise Errors::SecurityError.new("Invalid payload")
end
end
3. Configure mTLS in Rails with proper client verification
Use your web server (e.g., NGINX or Puma) to enforce client certificates, and ensure Rails only processes requests after successful mTLS verification. Below is an NGINX example and a Puma/Rack setup that rejects unverified clients.
# NGINX configuration snippet for mTLS
server {
listen 443 ssl;
ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/private/server.key;
ssl_client_certificate /etc/ssl/certs/ca.pem;
ssl_verify_client on;
ssl_verify_depth 2;
location /api/ {
# Only allow requests with a valid client cert
proxy_pass http://app_server;
proxy_set_header X-SSL-CERT $ssl_client_cert;
}
}
# config/puma.rb — ensure the app sees verified client certs
if ENV.key?('SSL_CLIENT_CERT')
cert = OpenSSL::X509::Certificate.new(Base64.strict_decode64(ENV['SSL_CLIENT_CERT']))
# Store verified client identity in request metadata for downstream use
map '/api' do
run ->(env) {
req = Rack::Request.new(env)
req.set_header('X-VERIFIED-MTLS-DN', cert.subject.to_s)
MyRailsApp.call(env)
}
end
end
4. Validate and restrict client certificates
Do not accept any certificate signed by a trusted CA; enforce extended key usage, hostname checks, and revocation (CRL/OCSP) where feasible. In Rails, you can inspect the verified certificate and enforce policies before routing to controllers.
# app/middleware/certificate_policy.rb
class CertificatePolicy
def initialize(app)
@app = app
end
def call(env)
if env.key?('HTTP_X_SSL_CERT')
cert_der = Base64.strict_decode64(env['HTTP_X_SSL_CERT'])
cert = OpenSSL::X509::Certificate.new(cert_der)
# Example policy: require specific extendedKeyUsage or SAN
unless cert.extensions.map(&:to_s).join.include?('1.3.6.1.5.5.7.3.2') # client auth EKU
return [403, { 'Content-Type' => 'text/plain' }, ['Forbidden: invalid client certificate']]
end
# Optionally check revocation via OCSP here
else
return [401, { 'Content-Type' => 'text/plain' }, ['Unverified client']]
end
@app.call(env)
end
end
By combining constant-time decryption, modern padding schemes, and strict mTLS policy enforcement, you reduce the attack surface for Bleichenbacher-style padding oracles even when client certificates are present.