This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch 4.1.x-fixes in repository https://gitbox.apache.org/repos/asf/cxf.git
commit d4cdcb12ef7eed0c8d54b3932d1a2b23b0192a5c Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Sep 3 09:28:08 2026 +0100 Add a fallback to prevent attacks on RSA 1.5 in the Jose code (#3435) (cherry picked from commit 7468e3c0b3ebd4e4020f5d36f404b09eb3eb4525) --- .../jose/jwe/WrappedKeyDecryptionAlgorithm.java | 49 ++++++++-- .../jose/jwe/JweCompactReaderWriterTest.java | 103 +++++++++++++++++++++ 2 files changed, 144 insertions(+), 8 deletions(-) diff --git a/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/jwe/WrappedKeyDecryptionAlgorithm.java b/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/jwe/WrappedKeyDecryptionAlgorithm.java index 3145a4377eb..12d4277f4dc 100644 --- a/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/jwe/WrappedKeyDecryptionAlgorithm.java +++ b/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/jwe/WrappedKeyDecryptionAlgorithm.java @@ -24,6 +24,7 @@ import java.util.logging.Logger; import org.apache.cxf.common.logging.LogUtils; import org.apache.cxf.rs.security.jose.jwa.AlgorithmUtils; +import org.apache.cxf.rs.security.jose.jwa.ContentAlgorithm; import org.apache.cxf.rs.security.jose.jwa.KeyAlgorithm; import org.apache.cxf.rt.security.crypto.CryptoUtils; import org.apache.cxf.rt.security.crypto.KeyProperties; @@ -41,6 +42,10 @@ public class WrappedKeyDecryptionAlgorithm implements KeyDecryptionProvider { this.cekDecryptionKey = cekDecryptionKey; this.supportedAlgo = supportedAlgo; this.unwrap = unwrap; + if (KeyAlgorithm.RSA1_5 == supportedAlgo) { + LOG.warning("The RSA1_5 JWE key encryption algorithm is deprecated: RSAES-PKCS1-v1_5 is" + + " vulnerable to padding oracle attacks, consider migrating to RSA-OAEP"); + } } public byte[] getDecryptedContentEncryptionKey(JweDecryptionInput jweDecryptionInput) { KeyProperties keyProps = new KeyProperties(getKeyEncryptionAlgorithm(jweDecryptionInput)); @@ -48,15 +53,43 @@ public class WrappedKeyDecryptionAlgorithm implements KeyDecryptionProvider { if (spec != null) { keyProps.setAlgoSpec(spec); } - if (!unwrap) { - keyProps.setBlockSize(getKeyCipherBlockSize()); - return CryptoUtils.decryptBytes(getEncryptedContentEncryptionKey(jweDecryptionInput), - getCekDecryptionKey(), keyProps); + byte[] fallbackCek = supportedAlgo == KeyAlgorithm.RSA1_5 + ? generateRandomContentEncryptionKey(jweDecryptionInput) : null; + try { + byte[] decryptedCek; + if (!unwrap) { + keyProps.setBlockSize(getKeyCipherBlockSize()); + decryptedCek = CryptoUtils.decryptBytes(getEncryptedContentEncryptionKey(jweDecryptionInput), + getCekDecryptionKey(), keyProps); + } else { + decryptedCek = CryptoUtils.unwrapSecretKey(getEncryptedContentEncryptionKey(jweDecryptionInput), + getKeyEncryptionAlgorithm(jweDecryptionInput), + getCekDecryptionKey(), + keyProps).getEncoded(); + } + return fallbackCek != null && decryptedCek.length != fallbackCek.length ? fallbackCek : decryptedCek; + } catch (SecurityException ex) { + if (fallbackCek != null) { + return fallbackCek; + } + throw ex; + } + } + + private static byte[] generateRandomContentEncryptionKey(JweDecryptionInput jweDecryptionInput) { + int keySizeBytes = 32; + try { + ContentAlgorithm ctAlgo = jweDecryptionInput.getJweHeaders().getContentEncryptionAlgorithm(); + if (ctAlgo != null) { + keySizeBytes = ctAlgo.getKeySizeBits() / 8; + if (AlgorithmUtils.isAesCbcHmac(ctAlgo.getJwaName())) { + keySizeBytes *= 2; + } + } + } catch (RuntimeException ex) { + // Keep a valid default size so malformed headers follow the normal rejection path. } - return CryptoUtils.unwrapSecretKey(getEncryptedContentEncryptionKey(jweDecryptionInput), - getKeyEncryptionAlgorithm(jweDecryptionInput), - getCekDecryptionKey(), - keyProps).getEncoded(); + return CryptoUtils.generateSecureRandomBytes(keySizeBytes); } protected Key getCekDecryptionKey() { diff --git a/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jwe/JweCompactReaderWriterTest.java b/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jwe/JweCompactReaderWriterTest.java index 094af892d23..20b808fecee 100644 --- a/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jwe/JweCompactReaderWriterTest.java +++ b/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jwe/JweCompactReaderWriterTest.java @@ -23,6 +23,7 @@ import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.util.Arrays; import javax.crypto.Cipher; import javax.crypto.SecretKey; @@ -206,6 +207,108 @@ public class JweCompactReaderWriterTest { String decryptedText = decryption.decrypt(jweContent).getContentText(); assertEquals(specPlainText, decryptedText); } + + @Test + public void testRSA15A128GCMDecryptionErrorsAreIndistinguishable() throws Exception { + assertRSA15DecryptionErrorsAreIndistinguishable(ContentAlgorithm.A128GCM); + } + + @Test + public void testRSA15A128CBCHS256DecryptionErrorsAreIndistinguishable() throws Exception { + assertRSA15DecryptionErrorsAreIndistinguishable(ContentAlgorithm.A128CBC_HS256); + } + + private void assertRSA15DecryptionErrorsAreIndistinguishable(ContentAlgorithm contentAlgorithm) + throws Exception { + Assume.assumeFalse(JavaUtils.isFIPSEnabled()); + RSAPublicKey publicKey = CryptoUtils.getRSAPublicKey(RSA_MODULUS_ENCODED_A1, + RSA_PUBLIC_EXPONENT_ENCODED_A1); + KeyEncryptionProvider keyEncryption = new RSAKeyEncryptionAlgorithm(publicKey, KeyAlgorithm.RSA1_5); + JweEncryptionProvider encryption; + if (AlgorithmUtils.isAesCbcHmac(contentAlgorithm.getJwaName())) { + encryption = new AesCbcHmacJweEncryption(contentAlgorithm, CONTENT_ENCRYPTION_KEY_A3, + INIT_VECTOR_A3, keyEncryption); + } else { + encryption = new JweEncryption(keyEncryption, + new AesGcmContentEncryptionAlgorithm(Arrays.copyOf(CONTENT_ENCRYPTION_KEY_A3, 16), INIT_VECTOR_A1, + contentAlgorithm)); + } + String jweContent = encryption.encrypt("test".getBytes(StandardCharsets.UTF_8), null); + + RSAPrivateKey privateKey = CryptoUtils.getRSAPrivateKey(RSA_MODULUS_ENCODED_A1, + RSA_PRIVATE_EXPONENT_ENCODED_A1); + for (boolean unwrap : new boolean[] {true, false}) { + KeyDecryptionProvider keyDecryption = new RSAKeyDecryptionAlgorithm(privateKey, KeyAlgorithm.RSA1_5, + unwrap); + JweDecryptionProvider decryption = AlgorithmUtils.isAesCbcHmac(contentAlgorithm.getJwaName()) + ? new AesCbcHmacJweDecryption(keyDecryption, contentAlgorithm) + : new JweDecryption(keyDecryption, new AesGcmContentDecryptionAlgorithm(contentAlgorithm)); + + assertEquals("test", decryption.decrypt(jweContent).getContentText()); + RuntimeException encryptedKeyFailure = getDecryptionFailure(decryption, + tamperJwePart(jweContent, 1, true)); + RuntimeException keyLengthFailure = getDecryptionFailure(decryption, + replaceJwePart(jweContent, 1, encryptInvalidCek(publicKey, contentAlgorithm))); + RuntimeException authenticationTagFailure = getDecryptionFailure(decryption, + tamperJwePart(jweContent, 4, false)); + assertEquals(getExceptionTypes(authenticationTagFailure), getExceptionTypes(encryptedKeyFailure)); + assertEquals(getExceptionTypes(authenticationTagFailure), getExceptionTypes(keyLengthFailure)); + } + } + + private static byte[] encryptInvalidCek(RSAPublicKey publicKey, ContentAlgorithm contentAlgorithm) + throws Exception { + int cekSize = contentAlgorithm.getKeySizeBits() / 8; + if (AlgorithmUtils.isAesCbcHmac(contentAlgorithm.getJwaName())) { + cekSize *= 2; + } + Cipher cipher = Cipher.getInstance(AlgorithmUtils.toJavaName(KeyAlgorithm.RSA1_5.getJwaName())); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + return cipher.doFinal(new byte[cekSize - 1]); + } + + private static String tamperJwePart(String jweContent, int partIndex, boolean truncate) throws Exception { + String[] parts = jweContent.split("\\.", -1); + byte[] value = Base64UrlUtility.decode(parts[partIndex]); + if (truncate) { + value = Arrays.copyOf(value, value.length - 1); + } else { + value[0] ^= 1; + } + return replaceJwePart(parts, partIndex, value); + } + + private static String replaceJwePart(String jweContent, int partIndex, byte[] value) { + return replaceJwePart(jweContent.split("\\.", -1), partIndex, value); + } + + private static String replaceJwePart(String[] parts, int partIndex, byte[] value) { + parts[partIndex] = Base64UrlUtility.encode(value); + return String.join(".", parts); + } + + private static RuntimeException getDecryptionFailure(JweDecryptionProvider decryption, String jweContent) { + try { + decryption.decrypt(jweContent); + } catch (RuntimeException ex) { + return ex; + } + fail("Decryption should have failed"); + return null; + } + + private static String getExceptionTypes(Throwable exception) { + StringBuilder types = new StringBuilder(); + while (exception != null) { + if (types.length() != 0) { + types.append(':'); + } + types.append(exception.getClass().getName()); + exception = exception.getCause(); + } + return types.toString(); + } + @Test public void testEncryptDecryptAesGcmWrapA128CBCHS256() throws Exception { //
