CWE-780: Use of RSA Algorithm without OAEP - Go
Overview
In Go, this weakness appears when RSA encryption uses the PKCS#1 v1.5 padding scheme instead of OAEP (Optimal Asymmetric Encryption Padding). RSA is an asymmetric encryption algorithm where public keys encrypt data and private keys decrypt it. Raw RSA, with no padding at all, is deterministic: the same plaintext always produces the same ciphertext. Padding schemes add the randomness and structure that make the encryption safe.
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 3072-bit RSA keys for new key material: NIST rates 2048-bit at 112-bit strength and accepts it only through 2030; 3072-bit gives 128-bit and stays valid beyond.
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. Modern protocols like TLS 1.3 use RSA-OAEP or prefer ECDH for key exchange specifically to avoid this vulnerability.
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 are indistinguishable from the attacker's perspective, so they leak nothing about padding validity. 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"
"fmt"
"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 pairs 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.
Message Size Validation
// SECURE - Validating plaintext size limits
import (
"crypto/rand"
"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 3072-bit key with SHA-256:
// Max plaintext = 384 bytes - 2*32 bytes - 2 = 318 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.
Common Pitfalls
- Switching new code to
rsa.EncryptOAEP/rsa.DecryptOAEPbut leaving an old handler that still callsrsa.DecryptPKCS1v15for "legacy client compatibility" -crypto/rsahappily supports both, so the Bleichenbacher oracle stays reachable through whichever endpoint still accepts PKCS#1 v1.5 ciphertexts. - Returning
err.Error()fromrsa.DecryptOAEPdirectly to an HTTP client or log line visible to the caller - Go returns the samecrypto/rsa: decryption errorfor a wrong OAEP hash, a wrong label, a corrupted ciphertext and one of the wrong length, so the error value carries no oracle on its own. A handler reintroduces one by adding a distinction it makes itself: rejecting a short ciphertext with a different message before the call, or reporting a separate failure when the decrypted key turns out to be the wrong size. - Encrypting a large payload by looping
rsa.EncryptOAEPover fixed-size chunks of the plaintext instead of using hybrid encryption - each chunk is independently OAEP-encrypted (so it isn't literally broken padding), but chunk boundaries and repeated-chunk patterns leak structure that a single AES-GCM-encrypted blob does not, and RSA's per-operation cost makes this far slower than wrapping one AES key. - Wiring a deterministic or
math/rand-backedio.Readerintorsa.EncryptOAEPfor reproducible tests, then reusing that same helper function in production code - OAEP's security depends on a fresh, unpredictable random seed fromcrypto/randfor every encryption; a deterministic source makes the "probabilistic" ciphertext predictable again even though the code still compiles and calls the OAEP function correctly.
Testing
Switching from PKCS#1 v1.5 to OAEP changes the ciphertext format, so the interesting failures are about data and payloads rather than the algorithm name. Assert each of these:
- Ciphertext round-trips. Encrypt with
rsa.EncryptOAEPand decrypt withrsa.DecryptOAEP, passing the same hash and label to both, and assertbytes.Equal(plaintext, recovered)rather thanerr == nil. A mismatched hash between the two calls fails only at decryption time, which in production means after the data is already stored. - The same plaintext encrypts to different ciphertext each time. OAEP is randomised. Encrypt one value twice and assert the outputs differ. Identical output means the padding is not being applied, which is the defect this CWE is about.
- Oversized payloads fail loudly. Encrypt a plaintext one byte longer than
the limit - 318 bytes for a 3072-bit key with SHA-256 - and assert
EncryptOAEPreturnscrypto/rsa: message too long for RSA key size(measured on Go 1.25). Also assert the largest payload the system actually sends is still under the limit: OAEP-SHA-256 leaves 318 bytes where PKCS#1 v1.5 left 373, so a record that encrypted before the change may not after it, and on a 2048-bit key the drop is 245 to 190. Discovering this limit from a production failure is the usual alternative. - Ciphertext written under PKCS#1 v1.5 still decrypts. Keep a fixture in the old format and assert the dual-read path returns the original plaintext, then assert new writes use OAEP.
- Decryption failures are indistinguishable to the caller. Assert that malformed padding, a wrong key, and truncated input all produce the same generic error and take comparable time. Distinct errors here are what makes a padding oracle exploitable, so a helpful error message reintroduces the vulnerability the padding change was meant to close.
Additional Resources
- Bleichenbacher's Attack Paper (1998)
- CWE-780: Use of RSA Algorithm without OAEP
- Go crypto/rsa Package
- NIST SP 800-57 Part 1 Rev 5: Recommendation for Key Management - Table 4 is the source for the 2030 date: 112-bit strength is Acceptable for applying protection (generating keys, encrypting, signing) through 2030 and Disallowed from 2031, while processing already-protected data stays permitted as legacy use
- OWASP Cryptographic Storage Cheat Sheet
- RFC 8017: PKCS #1 v2.2 - RSA padding schemes