This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch 3_0_x-fixes
in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git


The following commit(s) were added to refs/heads/3_0_x-fixes by this push:
     new 6520e896c Additional Bleichenbacher fix (#709)
6520e896c is described below

commit 6520e896c70992bcaaedc01a8f344dccce7aed26
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Sep 18 15:27:26 2026 +0100

    Additional Bleichenbacher fix (#709)
---
 .../wss4j/dom/processor/EncryptedKeyProcessor.java |  26 ++-
 .../dom/message/EncryptedKeyLengthOracleTest.java  | 205 +++++++++++++++++++++
 2 files changed, 230 insertions(+), 1 deletion(-)

diff --git 
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedKeyProcessor.java
 
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedKeyProcessor.java
index bd5ecc302..c7d147f5c 100644
--- 
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedKeyProcessor.java
+++ 
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedKeyProcessor.java
@@ -521,8 +521,8 @@ public class EncryptedKeyProcessor implements Processor {
      * first DataReference URI
      */
     protected static byte[] getRandomKey(Element refList, WSDocInfo wsDocInfo) 
throws WSSecurityException {
+        String algorithmURI = WSConstants.AES_128;
         try {
-            String algorithmURI = WSConstants.AES_128;
             String uri = getFirstDataRefURI(refList);
 
             if (uri != null) {
@@ -530,6 +530,19 @@ public class EncryptedKeyProcessor implements Processor {
                     EncryptionUtils.findEncryptedDataElement(wsDocInfo, uri);
                 algorithmURI = X509Util.getEncAlgo(ee);
             }
+        } catch (Throwable ex) {    //NOPMD
+            // Carry on with the default algorithm. The point of the key is 
only that it has a
+            // plausible length, and getRandomKey(String) falls back to AES 
anyway.
+            LOG.debug(ex.getMessage(), ex);
+        }
+        return getRandomKey(algorithmURI);
+    }
+
+    /**
+     * Generate a random secret key of exactly the length required by the 
given algorithm.
+     */
+    protected static byte[] getRandomKey(String algorithmURI) throws 
WSSecurityException {
+        try {
             // The key must have exactly the length required by the algorithm, 
otherwise it is
             // rejected later on, which would reveal that the key decryption 
failed.
             SecretKey k = KeyUtils.getKeyGenerator(algorithmURI).generateKey();
@@ -756,6 +769,17 @@ public class EncryptedKeyProcessor implements Processor {
             throw new WSSecurityException(
                 WSSecurityException.ErrorCode.UNSUPPORTED_ALGORITHM, ex, 
"badEncAlgo",
                 new Object[] {symEncAlgo});
+        } catch (WSSecurityException ex) {
+            // The key recovered from the EncryptedKey is not of the length 
this algorithm
+            // requires. Rejecting the message here would tell the sender that 
the private key
+            // operation yielded a well formed plaintext of some other length 
- exactly the signal
+            // a Bleichenbacher attack on the key transport is looking for, 
and one the attacker
+            // can provoke at will by choosing the EncryptedData algorithm, 
since a decryption
+            // that fails produces a random key of precisely the right length 
(see getRandomKey).
+            // Carry on with such a random key instead, so that both outcomes 
are the same
+            // outcome: the data does not decrypt.
+            LOG.debug("The key recovered from the EncryptedKey does not match 
{}", symEncAlgo);
+            symmetricKey = KeyUtils.prepareSecretKey(symEncAlgo, 
getRandomKey(symEncAlgo));
         }
 
         // Check for compliance against the defined AlgorithmSuite
diff --git 
a/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/EncryptedKeyLengthOracleTest.java
 
b/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/EncryptedKeyLengthOracleTest.java
new file mode 100644
index 000000000..9e7407a68
--- /dev/null
+++ 
b/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/EncryptedKeyLengthOracleTest.java
@@ -0,0 +1,205 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.wss4j.dom.message;
+
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPublicKey;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.security.auth.callback.CallbackHandler;
+
+import org.apache.wss4j.common.crypto.Crypto;
+import org.apache.wss4j.common.crypto.CryptoFactory;
+import org.apache.wss4j.common.crypto.CryptoType;
+import org.apache.wss4j.common.ext.WSSecurityException;
+import org.apache.wss4j.common.util.KeyUtils;
+import org.apache.wss4j.common.util.SOAPUtil;
+import org.apache.wss4j.common.util.XMLUtils;
+import org.apache.wss4j.dom.WSConstants;
+import org.apache.wss4j.dom.common.KeystoreCallbackHandler;
+import org.apache.wss4j.dom.engine.WSSConfig;
+import org.apache.wss4j.dom.engine.WSSecurityEngine;
+import org.apache.wss4j.dom.handler.RequestData;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+
+/**
+ * The receiver must not reveal whether the private key operation on an 
EncryptedKey produced a
+ * well formed PKCS#1 v1.5 plaintext. A decryption that fails is answered with 
a random key of
+ * exactly the length the EncryptedData algorithm requires, so that the 
failure surfaces as the
+ * data not decrypting. A decryption that succeeds but yields a plaintext of 
some other length
+ * must fail in the same way and not in one of its own - the attacker chooses 
the EncryptedData
+ * algorithm, and so chooses the length that counts as "other", which makes 
any distinction
+ * between the two a Bleichenbacher oracle on the key transport.
+ * <p/>
+ * The EncryptedData is GCM rather than CBC so that the comparison is not a 
lottery. Decrypting
+ * with a key that is merely wrong fails on the authentication tag every time, 
whereas the
+ * ISO10126 padding CBC uses is accepted by chance for about one random key in 
eighteen, and the
+ * garbage that comes out then fails in the XML parser instead - a difference 
that says nothing
+ * about the property under test.
+ */
+public class EncryptedKeyLengthOracleTest {
+
+    /** The block type PKCS#1 v1.5 uses for encryption. */
+    private static final byte BLOCK_TYPE = 0x02;
+
+    /** Any other block type makes the unpadding reject the plaintext. */
+    private static final byte NON_CONFORMING_BLOCK_TYPE = 0x03;
+
+    private final Crypto crypto;
+    private final CallbackHandler callbackHandler = new 
KeystoreCallbackHandler();
+    private final boolean isIBMJdK = 
System.getProperty("java.vendor").contains("IBM");
+
+    public EncryptedKeyLengthOracleTest() throws Exception {
+        WSSConfig.init();
+        crypto = CryptoFactory.getInstance("wss40.properties");
+    }
+
+    @Test
+    public void 
testWrongLengthPlaintextIsIndistinguishableFromFailedDecryption() throws 
Exception {
+        assumeFalse(isIBMJdK);
+
+        // A ciphertext that does not decrypt to a well formed plaintext at 
all: the block type
+        // is 0x03 where PKCS#1 v1.5 encryption requires 0x02, so the 
unpadding rejects it.
+        WSSecurityException failedDecryption =
+            decryptWithCipherValue(forgeCiphertext(NON_CONFORMING_BLOCK_TYPE, 
16));
+
+        // A ciphertext that decrypts to a perfectly well formed PKCS#1 v1.5 
plaintext, whose
+        // payload is simply not 16 bytes long - the length aes128-gcm 
requires. Only an attacker
+        // probing the key transport ever produces one of these.
+        WSSecurityException wrongLength = 
decryptWithCipherValue(forgeCiphertext(BLOCK_TYPE, 24));
+
+        assertEquals(failedDecryption.getErrorCode(), 
wrongLength.getErrorCode(),
+            "A well formed plaintext of the wrong length must not be 
distinguishable from a "
+            + "plaintext that is not well formed");
+        assertEquals(failedDecryption.getMessage(), wrongLength.getMessage(),
+            "A well formed plaintext of the wrong length must not be 
distinguishable from a "
+            + "plaintext that is not well formed");
+    }
+
+    /**
+     * The same, for a plaintext longer than the maximum key size rather than 
merely the wrong
+     * length: that is rejected by a different branch of 
KeyUtils.prepareSecretKey.
+     */
+    @Test
+    public void testOverlongPlaintextIsIndistinguishableFromFailedDecryption() 
throws Exception {
+        assumeFalse(isIBMJdK);
+
+        WSSecurityException failedDecryption =
+            decryptWithCipherValue(forgeCiphertext(NON_CONFORMING_BLOCK_TYPE, 
16));
+
+        WSSecurityException overlong = 
decryptWithCipherValue(forgeCiphertext(BLOCK_TYPE, 200));
+
+        assertEquals(failedDecryption.getErrorCode(), overlong.getErrorCode());
+        assertEquals(failedDecryption.getMessage(), overlong.getMessage());
+    }
+
+    /**
+     * Build an rsa-1_5 / aes128-gcm encrypted message, substitute the given 
bytes for the
+     * CipherValue of its EncryptedKey, and process it. The message never 
decrypts - the point is
+     * only how it fails.
+     */
+    private WSSecurityException decryptWithCipherValue(byte[] cipherValue) 
throws Exception {
+        Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
+        WSSecHeader secHeader = new WSSecHeader(doc);
+        secHeader.insertSecurityHeader();
+
+        WSSecEncrypt builder = new WSSecEncrypt(secHeader);
+        builder.setUserInfo("wss40");
+        builder.setKeyIdentifierType(WSConstants.BST_DIRECT_REFERENCE);
+        builder.setSymmetricEncAlgorithm(WSConstants.AES_128_GCM);
+        builder.setKeyEncAlgo(WSConstants.KEYTRANSPORT_RSA15);
+
+        KeyGenerator keyGen = 
KeyUtils.getKeyGenerator(WSConstants.AES_128_GCM);
+        SecretKey symmetricKey = keyGen.generateKey();
+        Document encryptedDoc = builder.build(crypto, symmetricKey);
+
+        Element encryptedKey =
+            XMLUtils.findElement(encryptedDoc.getDocumentElement(), 
"EncryptedKey", WSConstants.ENC_NS);
+        assertNotNull(encryptedKey);
+        Element cipherValueElement =
+            XMLUtils.findElement(encryptedKey, "CipherValue", 
WSConstants.ENC_NS);
+        assertNotNull(cipherValueElement);
+        cipherValueElement.setTextContent(
+            
org.apache.xml.security.utils.XMLUtils.encodeToString(cipherValue));
+
+        RequestData data = new RequestData();
+        data.setDecCrypto(crypto);
+        data.setSigVerCrypto(crypto);
+        data.setCallbackHandler(callbackHandler);
+        data.setAllowRSA15KeyTransportAlgorithm(true);
+
+        WSSecurityEngine secEngine = new WSSecurityEngine();
+        return assertThrows(WSSecurityException.class,
+            () -> secEngine.processSecurityHeader(encryptedDoc, data));
+    }
+
+    /**
+     * Raw-RSA encrypt EM = 0x00 || blockType || PS || 0x00 || M under the 
recipient's public key.
+     * With the PKCS#1 v1.5 block type the receiver's unpadding succeeds and 
hands back a payload
+     * of exactly payloadLength bytes, which is what a Bleichenbacher probe 
looks like once it
+     * finds a conforming ciphertext; with any other block type the unpadding 
rejects it.
+     */
+    private byte[] forgeCiphertext(byte blockType, int payloadLength) throws 
Exception {
+        int k = keySizeInBytes();
+        byte[] em = new byte[k];
+        em[0] = 0x00;
+        em[1] = blockType;
+        int paddingLength = k - 3 - payloadLength;
+        SecureRandom random = new SecureRandom();
+        for (int i = 0; i < paddingLength; i++) {
+            byte b;
+            do {
+                b = (byte)random.nextInt(256);
+            } while (b == 0);
+            em[2 + i] = b;
+        }
+        em[2 + paddingLength] = 0x00;
+        for (int i = 0; i < payloadLength; i++) {
+            em[3 + paddingLength + i] = (byte)(i + 1);
+        }
+
+        Cipher rsa = Cipher.getInstance("RSA/ECB/NoPadding");
+        rsa.init(Cipher.ENCRYPT_MODE, recipientPublicKey());
+        return rsa.doFinal(em);
+    }
+
+    private PublicKey recipientPublicKey() throws Exception {
+        CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);
+        cryptoType.setAlias("wss40");
+        X509Certificate[] certs = crypto.getX509Certificates(cryptoType);
+        assertNotNull(certs);
+        return certs[0].getPublicKey();
+    }
+
+    private int keySizeInBytes() throws Exception {
+        return (((RSAPublicKey)recipientPublicKey()).getModulus().bitLength() 
+ 7) / 8;
+    }
+}

Reply via email to