TLS Cipher Suite Selection: Security Analysis and Configuration Best Practices
Mamoun Tarsha-Kurdi
7 min read
Introduction
Cipher suite selection directly impacts TLS security. Understanding the components—key exchange, authentication, encryption, and MAC—is essential for secure configuration following modern best practices.
Cipher Suite Anatomy
Structure (TLS 1.2)
TLS_[KeyExchange]_[Authentication]_WITH_[Encryption]_[MAC]
Example: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- KeyExchange: ECDHE (Elliptic Curve Diffie-Hellman Ephemeral)
- Authentication: RSA (Server certificate)
- Encryption: AES_128_GCM (128-bit AES in GCM mode)
- MAC: SHA256 (implicit in GCM)
TLS 1.3 Simplification
TLS_[Encryption]_[Hash]
Example: TLS_AES_256_GCM_SHA384
- Encryption: AES_256_GCM
- Hash: SHA384
TLS 1.3 mandates:
- Only (EC)DHE key exchange (forward secrecy)
- Only AEAD ciphers (authenticated encryption)
- Authentication via certificates
Key Exchange Algorithms
RSA (Static) - DEPRECATED
# INSECURE: Static RSA key exchange
def rsa_key_exchange_no_forward_secrecy():
"""
Client encrypts pre-master secret with server's RSA public key
PROBLEM: No forward secrecy!
"""
# Client generates pre-master secret
pre_master_secret = os.urandom(48)
pre_master_secret[0:2] = b'\x03\x03' # TLS version
# Encrypt with server's RSA public key
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
encrypted_pms = server_rsa_public.encrypt(
pre_master_secret,
padding.PKCS1v15() # PKCS#1 v1.5 padding (has vulnerabilities!)
)
# VULNERABILITY: If server's private key ever compromised,
# ALL recorded sessions can be decrypted!
return encrypted_pms
DHE (Diffie-Hellman Ephemeral)
def dhe_key_exchange():
"""
Diffie-Hellman with ephemeral keys
Provides forward secrecy
"""
# Server parameters (usually pre-generated)
p = load_dhe_prime_2048bit() # Large prime
g = 2 # Generator
# Server generates ephemeral private key
server_private = random.randint(2, p-2)
server_public = pow(g, server_private, p)
# Client generates ephemeral private key
client_private = random.randint(2, p-2)
client_public = pow(g, client_private, p)
# Both compute same shared secret
server_shared = pow(client_public, server_private, p)
client_shared = pow(server_public, client_private, p)
assert server_shared == client_shared
# Ephemeral keys discarded after handshake
# → Forward secrecy achieved
return shared_secret
ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) - RECOMMENDED
def ecdhe_x25519_key_exchange():
"""
Modern ECDHE using Curve25519
Fast, secure, constant-time
"""
from cryptography.hazmat.primitives.asymmetric import x25519
# Server ephemeral key
server_private = x25519.X25519PrivateKey.generate()
server_public = server_private.public_key()
# Client ephemeral key
client_private = x25519.X25519PrivateKey.generate()
client_public = client_private.public_key()
# Compute shared secret (both sides)
server_shared = server_private.exchange(client_public)
client_shared = client_private.exchange(server_public)
assert server_shared == client_shared
# 32-byte shared secret, cryptographically strong
return shared_secret
# Performance comparison (modern CPU):
# RSA-2048 decrypt: ~2.8ms
# DHE-2048: ~1.5ms
# ECDHE X25519: ~0.05ms ← 56x faster than RSA!
Encryption Algorithms
Stream Ciphers - AVOID
# RC4 - BROKEN (RFC 7465)
# Multiple biases, plaintext recovery attacks
# MUST NOT be used
# ChaCha20 (stream cipher) - SECURE
# Modern alternative to AES, especially for mobile
def chacha20_poly1305_encrypt(key, nonce, plaintext, aad):
"""
ChaCha20-Poly1305 AEAD cipher
Recommended for ARM devices (no AES-NI)
"""
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
chacha = ChaCha20Poly1305(key) # 256-bit key
ciphertext = chacha.encrypt(nonce, plaintext, aad)
return ciphertext # Includes 128-bit Poly1305 MAC
Block Ciphers
# 3DES - DEPRECATED (64-bit blocks → Sweet32)
# AES - RECOMMENDED
def aes_gcm_encrypt(key, nonce, plaintext, aad):
"""
AES-GCM: Authenticated encryption (AEAD)
Recommended for hardware with AES-NI
"""
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
aesgcm = AESGCM(key) # 128 or 256-bit key
ciphertext = aesgcm.encrypt(nonce, plaintext, aad)
return ciphertext # Includes 128-bit GCM auth tag
# AES-CBC - AVOID (TLS 1.2)
# Vulnerable to padding oracle attacks (POODLE, Lucky13)
# Use AEAD modes only!
Message Authentication
HMAC (TLS 1.2 with non-AEAD)
def mac_then_encrypt_vulnerable():
"""
TLS 1.2 CBC mode: Encrypt-then-MAC
Vulnerable to timing attacks
"""
# Compute MAC
mac = hmac_sha256(mac_key, plaintext)
# Add PKCS#7 padding
padded = add_pkcs7_padding(plaintext + mac)
# Encrypt
ciphertext = aes_cbc_encrypt(enc_key, iv, padded)
# PROBLEM: Padding validation timing leaks information
return ciphertext
# TLS 1.3: Only AEAD (no separate MAC)
# AES-GCM, ChaCha20-Poly1305 include authentication
Secure Configuration
Modern Cipher Suite Ordering (2024)
RECOMMENDED_CIPHER_SUITES = [
# TLS 1.3 (mandatory AEAD + forward secrecy)
'TLS_AES_128_GCM_SHA256', # Fast, hardware-accelerated
'TLS_AES_256_GCM_SHA384', # Higher security margin
'TLS_CHACHA20_POLY1305_SHA256', # Mobiles/ARM (no AES-NI)
# TLS 1.2 fallback (ECDHE only)
'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256',
'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',
'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256',
# ECDSA certificates
'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384',
'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256',
]
FORBIDDEN_CIPHER_SUITES = [
# No forward secrecy
'TLS_RSA_*',
# Weak ciphers
'TLS_*_WITH_RC4_*', # RC4 broken
'TLS_*_WITH_3DES_*', # 64-bit blocks
'TLS_*_WITH_DES_*', # Completely broken
# Non-AEAD modes
'TLS_*_WITH_*_CBC_SHA', # Padding oracles
# Weak hashes
'TLS_*_MD5', # MD5 collisions
'TLS_*_SHA', # SHA-1 deprecated
# Export-grade
'TLS_*_EXPORT_*', # Deliberately weak
]
GnuTLS Priority String
// GnuTLS priority string syntax
const char *priority_string =
// Protocols
"SECURE256" // Equivalent to 256-bit security
":-VERS-SSL3.0" // Disable SSLv3
":-VERS-TLS1.0" // Disable TLS 1.0
":-VERS-TLS1.1" // Disable TLS 1.1
":+VERS-TLS1.2" // Enable TLS 1.2
":+VERS-TLS1.3" // Enable TLS 1.3
// Key exchange (forward secrecy only)
":-RSA" // Disable static RSA
":+ECDHE-RSA" // Enable ECDHE-RSA
":+ECDHE-ECDSA" // Enable ECDHE-ECDSA
// Ciphers (AEAD only)
":-CIPHER-ALL" // Remove all ciphers
":+AES-128-GCM" // Add AES-128-GCM
":+AES-256-GCM" // Add AES-256-GCM
":+CHACHA20-POLY1305" // Add ChaCha20-Poly1305
// MAC (implicit in AEAD)
":-MAC-ALL"
":+AEAD"
// Curves (modern only)
":-CURVE-ALL"
":+CURVE-X25519" // Recommended
":+CURVE-SECP256R1" // NIST P-256
":+CURVE-SECP384R1" // NIST P-384
// Signature algorithms
":-SIGN-ALL"
":+SIGN-RSA-PSS-SHA256"
":+SIGN-RSA-PSS-SHA384"
":+SIGN-ECDSA-SHA256"
":+SIGN-ECDSA-SHA384"
// Options
":+SAFE-RENEGOTIATION"
":+SESSION-TICKET"
"%DISABLE_SAFE_RENEGOTIATION" // Disable if not needed
"%NO_TICKETS"; // Disable if stateful preferred
// Set priority
gnutls_session_t session;
gnutls_init(&session, GNUTLS_SERVER);
int ret = gnutls_priority_set_direct(session, priority_string, NULL);
if (ret < 0) {
fprintf(stderr, "Priority error: %s\n", gnutls_strerror(ret));
}
OpenSSL Cipher String
// OpenSSL cipher string (TLS 1.2)
const char *cipher_string =
"ECDHE-ECDSA-AES128-GCM-SHA256:"
"ECDHE-RSA-AES128-GCM-SHA256:"
"ECDHE-ECDSA-AES256-GCM-SHA384:"
"ECDHE-RSA-AES256-GCM-SHA384:"
"ECDHE-ECDSA-CHACHA20-POLY1305:"
"ECDHE-RSA-CHACHA20-POLY1305:"
"!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA";
SSL_CTX *ctx = SSL_CTX_new(TLS_method());
SSL_CTX_set_cipher_list(ctx, cipher_string);
// TLS 1.3 ciphersuites (separate API)
const char *tls13_ciphers = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256";
SSL_CTX_set_ciphersuites(ctx, tls13_ciphers);
Testing Cipher Suite Support
import ssl
import socket
def test_cipher_suites(hostname, port=443):
"""
Test which cipher suites server supports
"""
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.set_ciphers('ALL:COMPLEMENTOFALL')
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cipher = ssock.cipher()
version = ssock.version()
print(f"Protocol: {version}")
print(f"Cipher: {cipher[0]}")
print(f"Bits: {cipher[2]}")
# Check for weak ciphers
weak_indicators = ['RC4', '3DES', 'DES', 'MD5', 'EXPORT', 'NULL']
cipher_name = cipher[0]
for indicator in weak_indicators:
if indicator in cipher_name:
print(f"WARNING: Weak cipher detected - {indicator}")
# Check for forward secrecy
if 'ECDHE' not in cipher_name and 'DHE' not in cipher_name:
print("WARNING: No forward secrecy!")
return cipher
# Scan all supported ciphers
def scan_all_ciphers(hostname):
"""Enumerate all supported ciphers"""
supported = []
all_ciphers = ssl._DEFAULT_CIPHERS.split(':')
for cipher in all_ciphers:
try:
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.set_ciphers(cipher)
with socket.create_connection((hostname, 443), timeout=5) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
supported.append(ssock.cipher()[0])
except:
pass
return supported
Performance Considerations
def benchmark_cipher_suites():
"""
Performance comparison (Intel i7 with AES-NI)
"""
results = {
'AES-128-GCM': {'throughput': '5 GB/s', 'latency': '0.1ms'},
'AES-256-GCM': {'throughput': '3.5 GB/s', 'latency': '0.12ms'},
'ChaCha20': {'throughput': '1.2 GB/s', 'latency': '0.3ms'},
}
# On ARM without AES-NI:
arm_results = {
'AES-128-GCM': {'throughput': '200 MB/s', 'latency': '1ms'},
'ChaCha20': {'throughput': '800 MB/s', 'latency': '0.4ms'},
}
# Recommendation:
# x86/x64 with AES-NI → AES-GCM
# ARM/Mobile → ChaCha20-Poly1305
Conclusion
Modern TLS cipher suite selection requires:
- TLS 1.3 preferred (simplified, secure by design)
- Forward secrecy mandatory (ECDHE/DHE only)
- AEAD ciphers only (AES-GCM, ChaCha20-Poly1305)
- Strong signatures (RSA-PSS, ECDSA with SHA-256+)
- Modern curves (X25519, P-256, P-384)
Disable all legacy algorithms: SSLv3, TLS 1.0/1.1, static RSA, CBC mode, RC4, 3DES, MD5, SHA-1.
Regular testing with SSL Labs and testssl.sh ensures configuration remains secure.
References
- RFC 8446 - TLS 1.3
- Mozilla SSL Configuration Generator
- NIST SP 800-52r2 - Guidelines for TLS Implementations
- GnuTLS Manual - Priority Strings
- OpenSSL Documentation - Cipher List Format