Skip to content

CWE-780: Use of RSA Without OAEP - Python

Overview

Using RSA encryption without OAEP (Optimal Asymmetric Encryption Padding) enables padding oracle attacks, chosen ciphertext attacks, and message malleability. In Python, this typically occurs when using the deprecated Crypto library (PyCrypto) or when not specifying padding parameters correctly with the cryptography library.

Primary Defence: Use the cryptography library with padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None) for RSA encryption/decryption, avoid deprecated PyCrypto and PKCS1_v1_5 padding entirely, always specify OAEP padding explicitly with SHA-256 or stronger hash functions, and consider using hybrid encryption (RSA for key exchange + Fernet or AES-GCM for data) to prevent padding oracle attacks.

Common Vulnerable Patterns

Using PKCS1_v1_5 Padding from PyCryptodome

# VULNERABLE - PKCS#1 v1.5 padding is deprecated and vulnerable

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from Crypto.Random import get_random_bytes

key = RSA.generate(2048)
cipher = PKCS1_v1_5.new(key)
message = b"Sensitive data"
ciphertext = cipher.encrypt(message)

# Attack example:

# Vulnerable to Bleichenbacher's padding oracle attack

# Attacker can decrypt ciphertext without private key by observing padding errors

Why this is vulnerable:

  • PKCS1_v1_5 uses legacy PKCS#1 v1.5 padding, broken since Bleichenbacher (1998).
  • Padding validation differences create a decryption oracle (errors/timing/exceptions).
  • Attackers can send many crafted ciphertexts and recover plaintext bit by bit.
  • Real-world exploits exist across TLS, XML encryption, and PKCS#11 systems.
  • PyCryptodome keeps PKCS1_v1_5 only for legacy compatibility and warns against it.

Using Legacy PyCrypto Library

# VULNERABLE - PyCrypto is deprecated and unmaintained since 2013

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5

key = RSA.generate(2048)
cipher = PKCS1_v1_5.new(key.publickey())
encrypted = cipher.encrypt(b"Secret message")

# Why vulnerable:

# - Library hasn't been updated since 2013

# - Uses vulnerable PKCS#1 v1.5 padding by default

# - Contains known security vulnerabilities

Why this is vulnerable:

  • PyCrypto is unmaintained since 2013 with no security patches.
  • Known CVEs include memory corruption, timing issues, and weak RNGs.
  • It uses PKCS#1 v1.5, enabling Bleichenbacher-style attacks.
  • Attackers can fingerprint the dependency and apply public exploits.
  • It lacks modern standards support and breaks on newer Python versions.

Using cryptography Library with PKCS1v15 Padding

# VULNERABLE - Explicitly using PKCS1v15 instead of OAEP

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend

private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
    backend=default_backend()
)
public_key = private_key.public_key()

message = b"Confidential data"
ciphertext = public_key.encrypt(
    message,
    padding.PKCS1v15()  # Vulnerable padding!
)

# Why vulnerable:

# - PKCS#1 v1.5 padding enables timing attacks

# - Deterministic encryption (same plaintext → same ciphertext)

# - Vulnerable to Bleichenbacher's attack

Why this is vulnerable:

  • padding.PKCS1v15() reintroduces PKCS#1 v1.5 padding oracles.
  • Deterministic padding makes identical plaintexts produce identical ciphertexts.
  • Padding vs decryption errors can leak an oracle via exceptions or timing.
  • Web apps often surface these signals through error responses at scale.

Using Weak Hash Algorithm with OAEP

# VULNERABLE - Using deprecated SHA-1 hash with OAEP

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes

private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)
public_key = private_key.public_key()

ciphertext = public_key.encrypt(
    b"Message",
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA1()),  # Weak!
        algorithm=hashes.SHA1(),  # Deprecated!
        label=None
    )
)

# Why vulnerable:

# - SHA-1 is cryptographically broken

# - Reduces the security level of OAEP

**Why this is vulnerable:**
- OAEP is fine, but SHA-1 is broken (practical collisions).
- OAEP relies on a strong hash; SHA-1 undermines that security proof.
- SHA-1 offers only ~80-bit collision security (below modern minimums).
- Standards prohibit SHA-1, risking audit failures and compliance issues.

Secure Patterns

Using cryptography Library with OAEP and SHA-256

# SECURE - Modern cryptography library with OAEP padding and SHA-256

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend

# Generate RSA key pair

private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,  # Minimum 2048 bits
    backend=default_backend()
)
public_key = private_key.public_key()

message = b"Sensitive data to encrypt"

# Encrypt with OAEP padding and SHA-256

ciphertext = public_key.encrypt(
    message,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

# Decrypt with OAEP padding

plaintext = private_key.decrypt(
    ciphertext,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

assert plaintext == message

Why this works:

  • OAEP per RFC 8017 makes encryption probabilistic, blocking pattern analysis.
  • MGF1(SHA-256) spreads randomness across the padded message.
  • SHA-256 provides collision resistance for the OAEP security proof.
  • label=None is the standard interoperable default.

Using PyCryptodome with OAEP (Alternative to cryptography)

# SECURE - PyCryptodome (modern replacement for PyCrypto) with OAEP

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP  # Use OAEP, not PKCS1_v1_5
from Crypto.Hash import SHA256

# Generate RSA key pair

key = RSA.generate(2048)

# Create cipher with OAEP padding

cipher = PKCS1_OAEP.new(key, hashAlgo=SHA256)

message = b"Confidential message"

# Encrypt with OAEP

ciphertext = cipher.encrypt(message)

# Decrypt with OAEP

plaintext = cipher.decrypt(ciphertext)

assert plaintext == message

Why this works:

  • PKCS1_OAEP adds randomness, making RSA semantically secure.
  • Explicit hashAlgo=SHA256 avoids weak default hashes.
  • Key and hash are bound at construction to prevent misconfiguration.
  • Maintained fork with security fixes; drop-in for legacy PyCrypto code.

Using OAEP with SHA-384 for Higher Security

# SECURE - OAEP with SHA-384 for enhanced security

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes

private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=4096,  # Higher key size for long-term security
)
public_key = private_key.public_key()

message = b"Highly sensitive data"

# Encrypt with OAEP using SHA-384

ciphertext = public_key.encrypt(
    message,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA384()),
        algorithm=hashes.SHA384(),
        label=None
    )
)

# Decrypt

plaintext = private_key.decrypt(
    ciphertext,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA384()),
        algorithm=hashes.SHA384(),
        label=None
    )
)

Why this works:

  • SHA-384 provides a higher security margin than SHA-256.
  • 4096-bit RSA + SHA-384 meets high-assurance requirements.
  • Trade-off: larger OAEP overhead and slightly smaller plaintext size.
  • Use for long-retention or high-value data; SHA-256 is fine for most apps.

Hybrid Encryption for Large Data

# SECURE - Hybrid encryption: RSA-OAEP for key, AES-GCM for data

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
import os

def hybrid_encrypt(public_key, plaintext):
    """Encrypt large data using hybrid encryption"""
    # Generate random AES key
    aes_key = AESGCM.generate_key(bit_length=256)

    # Encrypt data with AES-GCM
    aesgcm = AESGCM(aes_key)
    nonce = os.urandom(12)
    ciphertext = aesgcm.encrypt(nonce, plaintext, None)

    # Encrypt AES key with RSA-OAEP
    encrypted_key = public_key.encrypt(
        aes_key,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )

    return {
        'encrypted_key': encrypted_key,
        'nonce': nonce,
        'ciphertext': ciphertext
    }

# Encrypts data of any size securely

large_data = b"Very large confidential document..." * 1000
encrypted = hybrid_encrypt(public_key, large_data)

Why this works:

  • RSA size limits make direct encryption impractical for large data.
  • AES-GCM handles bulk encryption efficiently and adds integrity.
  • RSA-OAEP wraps only the short AES key within RSA limits.
  • Mirrors TLS-style hybrid design for scalable, secure encryption.

Remediation Strategy

PRIORITY 1: Use cryptography Library with OAEP (PRIMARY FIX)

Use the modern cryptography library (not the deprecated Crypto/PyCrypto) with explicit OAEP padding.

Install cryptography

pip install cryptography

RSA Encryption with OAEP

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend

# Generate RSA key pair

private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,  # Minimum 2048 bits
    backend=default_backend()
)
public_key = private_key.public_key()

# Message to encrypt (must be bytes)

message = b"Sensitive data to encrypt"

# SECURE - Encrypt with OAEP padding

ciphertext = public_key.encrypt(
    message,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None  # Optional label, usually None
    )
)

# SECURE - Decrypt with OAEP padding

plaintext = private_key.decrypt(
    ciphertext,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

assert plaintext == message

PRIORITY 2: Hybrid Encryption for Large Data

RSA is limited to small data. For 2048-bit RSA with SHA-256, max plaintext is ~190 bytes. Use hybrid encryption for larger data.

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
import os

def hybrid_encrypt(public_key, plaintext):
    """
    Encrypt large data using hybrid encryption:

    - Generate random AES key
    - Encrypt data with AES-GCM
    - Encrypt AES key with RSA-OAEP
    """
    # Generate random 256-bit AES key
    aes_key = AESGCM.generate_key(bit_length=256)

    # Encrypt data with AES-GCM
    aesgcm = AESGCM(aes_key)
    nonce = os.urandom(12)  # 96-bit nonce for GCM
    ciphertext = aesgcm.encrypt(nonce, plaintext, None)

    # Encrypt AES key with RSA-OAEP
    encrypted_key = public_key.encrypt(
        aes_key,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )

    return {
        'encrypted_key': encrypted_key,
        'nonce': nonce,
        'ciphertext': ciphertext
    }

def hybrid_decrypt(private_key, encrypted_data):
    """Decrypt hybrid-encrypted data"""
    # Decrypt AES key with RSA-OAEP
    aes_key = private_key.decrypt(
        encrypted_data['encrypted_key'],
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )

    # Decrypt data with AES-GCM
    aesgcm = AESGCM(aes_key)
    plaintext = aesgcm.decrypt(
        encrypted_data['nonce'],
        encrypted_data['ciphertext'],
        None
    )

    return plaintext

# Example usage

large_data = b"This can be megabytes of data" * 1000
encrypted = hybrid_encrypt(public_key, large_data)
decrypted = hybrid_decrypt(private_key, encrypted)
assert decrypted == large_data

PRIORITY 3: RSA Signatures with PSS (Not OAEP)

For signatures (not encryption), use PSS padding instead of OAEP.

from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes

message = b"Document to sign"

# SECURE - Sign with PSS padding

signature = private_key.sign(
    message,
    padding.PSS(
        mgf=padding.MGF1(hashes.SHA256()),
        salt_length=padding.PSS.MAX_LENGTH  # Recommended
    ),
    hashes.SHA256()
)

# Verify signature

try:
    public_key.verify(
        signature,
        message,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH
        ),
        hashes.SHA256()
    )
    print("Signature valid!")
except Exception:
    print("Signature invalid!")

Additional Resources