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;
+ }
+}