Add a fallback to prevent attacks on RSA 1.5 in the Jose code (#3435)
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 3145a43..12d4277 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 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 @@
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 @@
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;
}
- return CryptoUtils.unwrapSecretKey(getEncryptedContentEncryptionKey(jweDecryptionInput),
- getKeyEncryptionAlgorithm(jweDecryptionInput),
- getCekDecryptionKey(),
- keyProps).getEncoded();
+ }
+
+ 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.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 67b9052..b267063 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.ECPublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
+import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
@@ -263,6 +264,108 @@
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 {
//fips: CBC mode not supported