Skip to content

CWE-780: Use of RSA Algorithm without OAEP - Go

Overview

Use of RSA without OAEP vulnerabilities in Go applications occur when RSA encryption uses the insecure PKCS#1 v1.5 padding scheme instead of the secure OAEP (Optimal Asymmetric Encryption Padding) scheme. RSA is an asymmetric encryption algorithm where public keys encrypt data and private keys decrypt it. However, raw RSA (without padding) is mathematically weak and vulnerable to various attacks. Padding schemes add randomness and structure to prevent these attacks.

PKCS#1 v1.5 padding, the older scheme, has been shown to be vulnerable to padding oracle attacks (Bleichenbacher's attack from 1998). These attacks allow attackers to decrypt ciphertexts by repeatedly sending modified ciphertexts to a server and observing whether decryption succeeds or fails. Even timing differences in error messages can leak enough information to recover plaintext. OAEP (introduced in PKCS#1 v2.0) uses cryptographic hash functions and mask generation to provide provable security against adaptive chosen-ciphertext attacks.

Go's crypto/rsa package provides both vulnerable and secure functions. rsa.EncryptPKCS1v15 and rsa.DecryptPKCS1v15 implement the insecure scheme, maintained for backward compatibility with legacy systems. rsa.EncryptOAEP and rsa.DecryptOAEP implement the secure scheme and should be used for all new code. Modern security standards (NIST, OWASP) deprecate PKCS#1 v1.5 for encryption and mandate OAEP.

Primary Defence: Use rsa.EncryptOAEP and rsa.DecryptOAEP with SHA-256 or better hash functions. Never use rsa.EncryptPKCS1v15 or rsa.DecryptPKCS1v15 for new code. For hybrid encryption (RSA for key exchange, AES for data), encrypt the AES key with RSA-OAEP. Use 2048-bit or 4096-bit RSA keys.

Common Vulnerable Patterns

Using PKCS#1 v1.5 Padding

// VULNERABLE - RSA with insecure PKCS#1 v1.5 padding
package main

import (
    "crypto/rand"
    "crypto/rsa"
)

func encryptDataInsecure(publicKey *rsa.PublicKey, plaintext []byte) ([]byte, error) {
    // DANGEROUS: Using deprecated PKCS#1 v1.5 padding
    ciphertext, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, plaintext)
    if err != nil {
        return nil, err
    }

    return ciphertext, nil
}

func decryptDataInsecure(privateKey *rsa.PrivateKey, ciphertext []byte) ([]byte, error) {
    // VULNERABLE: Padding oracle attacks possible
    plaintext, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, ciphertext)
    if err != nil {
        return nil, err
    }

    return plaintext, nil
}

// VULNERABILITY:
// Padding oracle attacks (Bleichenbacher's attack):
// 1. Attacker sends modified ciphertext to server
// 2. Observes error messages or timing differences
// 3. Determines if padding is valid
// 4. Repeats millions of times to decrypt message

Why this is vulnerable: PKCS#1 v1.5 padding has a specific structure that can be detected through error messages or timing analysis. When decryption fails due to invalid padding, the error differs from other decryption failures. Attackers exploit this oracle - they send modified ciphertexts and observe whether padding validation succeeds. Over many queries (typically 1-2 million), they can fully decrypt the ciphertext without the private key. Even constant-time implementations are difficult to achieve correctly. The vulnerability is inherent to the padding scheme design.

Padding Oracle Attack via Error Messages

// VULNERABLE - Leaking padding validation results
import (
    "crypto/rsa"
    "fmt"
    "net/http"
)

func decryptAPIHandler(w http.ResponseWriter, r *http.Request) {
    // Get encrypted data from request
    ciphertext := []byte(r.FormValue("ciphertext"))

    // Load private key (simplified)
    privateKey := loadPrivateKey()

    // DANGEROUS: Different error messages leak padding validation
    plaintext, err := rsa.DecryptPKCS1v15(nil, privateKey, ciphertext)
    if err != nil {
        // VULNERABLE: Error message might indicate padding vs other failures
        http.Error(w, fmt.Sprintf("Decryption failed: %v", err), http.StatusBadRequest)
        return
    }

    w.Write([]byte("Success: " + string(plaintext)))
}

// ATTACK:
// Attacker modifies ciphertext byte by byte
// Observes different error messages or response times
// Infers padding validity
// Gradually recovers plaintext

Why this is vulnerable: Distinguishable error messages or response times create a padding oracle. If padding validation fails, the error happens early. If padding is valid but decryption fails (wrong key, corrupted data), the error happens later. Even a few microseconds difference is exploitable with enough samples. Returning the error message to the user directly reveals the validation result. Attackers automate the process, sending thousands of modified ciphertexts and analyzing responses. Each successful padding validation narrows the plaintext possibilities.

Using PKCS#1 v1.5 for Hybrid Encryption

// VULNERABLE - Hybrid encryption with weak RSA padding
import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "crypto/rsa"
    "io"
)

func hybridEncryptInsecure(publicKey *rsa.PublicKey, plaintext []byte) ([]byte, []byte, error) {
    // Generate AES key
    aesKey := make([]byte, 32)
    io.ReadFull(rand.Reader, aesKey)

    // Encrypt data with AES
    block, _ := aes.NewCipher(aesKey)
    gcm, _ := cipher.NewGCM(block)
    nonce := make([]byte, gcm.NonceSize())
    io.ReadFull(rand.Reader, nonce)
    ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)

    // DANGEROUS: Encrypt AES key with insecure RSA padding
    encryptedKey, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, aesKey)
    if err != nil {
        return nil, nil, err
    }

    return encryptedKey, ciphertext, nil
}

// VULNERABILITY:
// Even though AES-GCM is secure, the weak RSA encryption
// of the AES key undermines the entire system
// Attacker recovers AES key via padding oracle
// Decrypts all data encrypted with that key

Why this is vulnerable: Hybrid encryption is only as strong as its weakest component. While AES-256-GCM provides strong symmetric encryption, using PKCS#1 v1.5 for the RSA key encryption exposes the AES key to padding oracle attacks. Once attackers recover the AES key (by exploiting the RSA padding oracle), they can decrypt all data encrypted with that key. The entire security model collapses. Modern protocols like TLS 1.3 use RSA-OAEP or prefer ECDH for key exchange specifically to avoid this vulnerability.

Timing Attack Surface

// VULNERABLE - Timing-based padding oracle
import (
    "crypto/rsa"
    "crypto/subtle"
    "time"
)

func decryptWithTiming(privateKey *rsa.PrivateKey, ciphertext []byte) ([]byte, bool) {
    startTime := time.Now()

    // PKCS#1 v1.5 decryption
    plaintext, err := rsa.DecryptPKCS1v15(nil, privateKey, ciphertext)

    elapsed := time.Since(startTime)

    // VULNERABLE: Even with constant-time checks, padding validation time differs
    if err != nil {
        return nil, false
    }

    // Additional validation adds more timing variation
    if !isValidFormat(plaintext) {
        return nil, false
    }

    return plaintext, true
}

func isValidFormat(data []byte) bool {
    // Format checking adds more timing leakage
    return len(data) > 0
}

// ATTACK:
// Measure response times for many modified ciphertexts
// Statistical analysis reveals timing patterns
// Distinguish padding validation from format validation
// Exploit timing differences to recover plaintext

Why this is vulnerable: Even with careful implementation, PKCS#1 v1.5 padding validation has subtle timing variations. Padding validation occurs early in decryption, while format validation occurs later. Statistical analysis of thousands of timing measurements reveals patterns. Attackers use high-precision timers and statistical methods to detect microsecond differences. Server processing time variations (CPU load, network latency) create noise, but averaging many samples reveals the underlying timing oracle. Constant-time implementations are extremely difficult to verify.

Secure Patterns

RSA-OAEP for Encryption

// SECURE - RSA with OAEP padding
package main

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha256"
)

func encryptWithOAEP(publicKey *rsa.PublicKey, plaintext []byte) ([]byte, error) {
    // SECURE: Use OAEP with SHA-256
    hash := sha256.New()

    ciphertext, err := rsa.EncryptOAEP(
        hash,
        rand.Reader,
        publicKey,
        plaintext,
        nil, // Optional label
    )
    if err != nil {
        return nil, err
    }

    return ciphertext, nil
}

func decryptWithOAEP(privateKey *rsa.PrivateKey, ciphertext []byte) ([]byte, error) {
    // SECURE: OAEP decryption
    hash := sha256.New()

    plaintext, err := rsa.DecryptOAEP(
        hash,
        rand.Reader,
        privateKey,
        ciphertext,
        nil,
    )
    if err != nil {
        return nil, err
    }

    return plaintext, nil
}

Why this works: OAEP uses a mask generation function with cryptographic hash functions (SHA-256) to add randomness and structure that prevents padding oracle attacks. The padding scheme is provably secure against adaptive chosen-ciphertext attacks (IND-CCA2 security). Errors during OAEP decryption don't leak information about padding validity - all errors are indistinguishable from the attacker's perspective. SHA-256 is recommended over SHA-1 (which has known weaknesses). The optional label parameter allows binding ciphertext to specific contexts but is rarely needed.

Secure Hybrid Encryption

// SECURE - Hybrid encryption with RSA-OAEP and AES-GCM
import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha256"
    "io"
)

func hybridEncrypt(publicKey *rsa.PublicKey, plaintext []byte) ([]byte, []byte, error) {
    // Generate random AES-256 key
    aesKey := make([]byte, 32)
    if _, err := io.ReadFull(rand.Reader, aesKey); err != nil {
        return nil, nil, err
    }

    // Encrypt data with AES-GCM
    block, err := aes.NewCipher(aesKey)
    if err != nil {
        return nil, nil, err
    }

    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, nil, err
    }

    nonce := make([]byte, gcm.NonceSize())
    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return nil, nil, err
    }

    dataCiphertext := gcm.Seal(nonce, nonce, plaintext, nil)

    // SECURE: Encrypt AES key with RSA-OAEP
    hash := sha256.New()
    keyCiphertext, err := rsa.EncryptOAEP(
        hash,
        rand.Reader,
        publicKey,
        aesKey,
        nil,
    )
    if err != nil {
        return nil, nil, err
    }

    return keyCiphertext, dataCiphertext, nil
}

func hybridDecrypt(privateKey *rsa.PrivateKey, keyCiphertext, dataCiphertext []byte) ([]byte, error) {
    // SECURE: Decrypt AES key with RSA-OAEP
    hash := sha256.New()
    aesKey, err := rsa.DecryptOAEP(
        hash,
        rand.Reader,
        privateKey,
        keyCiphertext,
        nil,
    )
    if err != nil {
        return nil, err
    }

    // Decrypt data with recovered AES key
    block, err := aes.NewCipher(aesKey)
    if err != nil {
        return nil, err
    }

    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }

    nonceSize := gcm.NonceSize()
    if len(dataCiphertext) < nonceSize {
        return nil, fmt.Errorf("ciphertext too short")
    }

    nonce, ciphertext := dataCiphertext[:nonceSize], dataCiphertext[nonceSize:]

    plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
    if err != nil {
        return nil, err
    }

    return plaintext, nil
}

Why this works: RSA-OAEP securely encrypts the AES key, preventing padding oracle attacks on the key exchange. AES-256-GCM efficiently encrypts arbitrary-length data with authentication. This combination provides the best of both worlds: RSA's public-key encryption (no shared secret needed) with AES's performance and unlimited message size. The AES key is ephemeral (generated per encryption), so even if one key is compromised, other messages remain secure. This is the standard pattern for public-key encryption of large data.

Error Handling Without Information Leakage

// SECURE - Consistent error handling
import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha256"
    "errors"
)

var ErrDecryptionFailed = errors.New("decryption failed")

func decryptSecurely(privateKey *rsa.PrivateKey, ciphertext []byte) ([]byte, error) {
    hash := sha256.New()

    plaintext, err := rsa.DecryptOAEP(
        hash,
        rand.Reader,
        privateKey,
        ciphertext,
        nil,
    )

    // SECURE: Generic error message
    if err != nil {
        return nil, ErrDecryptionFailed
    }

    // Additional validation with same error
    if len(plaintext) == 0 {
        return nil, ErrDecryptionFailed
    }

    return plaintext, nil
}

func decryptHandler(w http.ResponseWriter, r *http.Request) {
    ciphertext := getCiphertextFromRequest(r)
    privateKey := loadPrivateKey()

    plaintext, err := decryptSecurely(privateKey, ciphertext)

    // SECURE: Same error response for all failures
    if err != nil {
        http.Error(w, "Decryption failed", http.StatusBadRequest)
        return
    }

    // Process plaintext
    w.Write([]byte("Success"))
}

func getCiphertextFromRequest(r *http.Request) []byte {
    return []byte(r.FormValue("data"))
}

func loadPrivateKey() *rsa.PrivateKey {
    key, _ := rsa.GenerateKey(rand.Reader, 2048)
    return key
}

Why this works: All decryption errors return the same generic error message (ErrDecryptionFailed), preventing attackers from distinguishing padding validation failures from other failures. OAEP's error handling is already constant-time and indistinguishable, but this adds defense-in-depth. HTTP responses are identical for all error cases - same status code, same message, ideally same processing time. Logging should not include detailed error information that could leak to attackers. This prevents even subtle information leakage through error messages.

Using 2048+ Bit Keys

// SECURE - Generating strong RSA keys
import (
    "crypto/rand"
    "crypto/rsa"
)

func generateStrongRSAKey() (*rsa.PrivateKey, error) {
    // SECURE: 2048-bit minimum, 4096-bit for long-term secrets
    keySize := 2048

    privateKey, err := rsa.GenerateKey(rand.Reader, keySize)
    if err != nil {
        return nil, err
    }

    return privateKey, nil
}

func generateHighSecurityKey() (*rsa.PrivateKey, error) {
    // SECURE: 4096-bit for highly sensitive data
    privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
    if err != nil {
        return nil, err
    }

    return privateKey, nil
}

Why this works: RSA key size directly impacts security. NIST requires minimum 2048-bit keys (until 2030), with 3072-bit recommended for data that must remain secure beyond 2030. 4096-bit keys provide additional security margin for highly sensitive data or long-term encryption. Smaller keys (1024-bit) can be factored with significant computational resources. Combined with OAEP, strong key sizes provide comprehensive security against current and foreseeable attacks. Key generation uses crypto/rand for cryptographically secure randomness.

Message Size Validation

// SECURE - Validating plaintext size limits
import (
    "crypto/rsa"
    "crypto/sha256"
    "fmt"
)

func encryptWithSizeCheck(publicKey *rsa.PublicKey, plaintext []byte) ([]byte, error) {
    // SECURE: Calculate maximum message size for OAEP
    // RSA key size - 2*hash size - 2
    hash := sha256.New()
    maxSize := publicKey.Size() - 2*hash.Size() - 2

    if len(plaintext) > maxSize {
        return nil, fmt.Errorf("plaintext too large: max %d bytes for %d-bit key",
            maxSize, publicKey.Size()*8)
    }

    return rsa.EncryptOAEP(hash, rand.Reader, publicKey, plaintext, nil)
}

// For 2048-bit key with SHA-256:
// Max plaintext = 256 bytes - 2*32 bytes - 2 = 190 bytes
//
// For larger data, use hybrid encryption (RSA + AES)

Why this works: RSA-OAEP has message size limits based on key size and hash function. Attempting to encrypt messages larger than the limit causes errors. Checking size before encryption provides clear error messages and prevents unexpected failures. For most use cases, hybrid encryption (RSA-OAEP for a symmetric key, AES-GCM for data) is preferred since RSA is slow and has size limits. The size check is explicit about maximum plaintext size, helping developers understand RSA limitations.

Additional Resources