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

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

commit ae9c9fe2e15222af14a8344ea9420fdb7ae83696
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Sep 21 10:05:24 2026 +0100

    Extend the EncryptedKey random-key fallback beyond decryptDataRef
---
 THREAT-MODEL.md                                    | 14 +++-
 .../dom/processor/EncryptedDataProcessor.java      | 13 +++-
 .../wss4j/dom/processor/EncryptedKeyProcessor.java | 42 ++++++++----
 .../dom/processor/ReferenceListProcessor.java      |  4 +-
 .../org/apache/wss4j/dom/str/STRParserResult.java  | 17 +++++
 .../wss4j/dom/str/SecurityTokenRefSTRParser.java   |  1 +
 .../dom/message/EncryptedKeyLengthOracleTest.java  | 78 +++++++++++++++++++++-
 7 files changed, 150 insertions(+), 19 deletions(-)

diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md
index 82d8ba8d6..32fdd3c91 100644
--- a/THREAT-MODEL.md
+++ b/THREAT-MODEL.md
@@ -484,9 +484,21 @@ on each is captured in §14 Q10–Q11.
   / timing signal between "padding malformed" and "padding well-formed
   but plaintext rejected" that lets the attacker iteratively recover the
   symmetric key.
+- **Mechanism**: a decryption that fails yields a random key of the
+  length the algorithm requires, and a decryption that succeeds but
+  yields a key of some other length is answered with the same. The
+  length the attacker's message declares is theirs to choose, so this
+  has to hold wherever a key recovered from an EncryptedKey is prepared
+  — in `EncryptedKeyProcessor` for the EncryptedData its ReferenceList
+  names, and in `EncryptedDataProcessor` and `ReferenceListProcessor`
+  for an EncryptedKey embedded in a KeyInfo, named by a RetrievalMethod,
+  or reached through a SecurityTokenReference. It does not extend to a
+  key taken from a token or supplied by the CallbackHandler, which
+  carries no such signal.
 - **Severity**: **security-critical**, `VALID` per §13.
 - *(documented: CVE-2015-0226 advisory; `best_practice.adoc`
-  §"Use RSA-OAEP for the Key Transport Algorithm")*
+  §"Use RSA-OAEP for the Key Transport Algorithm";
+  `ws-security-dom/src/test/java/.../EncryptedKeyLengthOracleTest.java`)*
 
 ### P5 — Replay protection on inbound UsernameToken nonces, Timestamps, and 
SAML2 OneTimeUse assertions
 
diff --git 
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedDataProcessor.java
 
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedDataProcessor.java
index 5d9735c5c..3f6d6e317 100644
--- 
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedDataProcessor.java
+++ 
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedDataProcessor.java
@@ -115,7 +115,9 @@ public class EncryptedDataProcessor implements Processor {
             STRParserResult parserResult = 
strParser.parseSecurityTokenReference(parameters);
             byte[] secretKey = parserResult.getSecretKey();
             principal = parserResult.getPrincipal();
-            key = KeyUtils.prepareSecretKey(symEncAlgo, secretKey);
+            key = parserResult.isSecretKeyFromEncryptedKey()
+                ? 
EncryptedKeyProcessor.prepareSecretKeyFromEncryptedKey(symEncAlgo, secretKey)
+                : KeyUtils.prepareSecretKey(symEncAlgo, secretKey);
             encrKeyResults = new ArrayList<>();
         } else if (encryptedKeyElement != null && data.getWssConfig() != null) 
{
             WSSConfig wssConfig = data.getWssConfig();
@@ -128,7 +130,10 @@ public class EncryptedDataProcessor implements Processor {
             }
             byte[] symmKey =
                 
(byte[])encrKeyResults.get(0).get(WSSecurityEngineResult.TAG_SECRET);
-            key = KeyUtils.prepareSecretKey(symEncAlgo, symmKey);
+            //An EncryptedKey inline in this KeyInfo carries no ReferenceList 
of its own, so a
+            //failed decryption yields a random key of the default length 
rather than of the
+            //length this EncryptedData needs. Do not let that difference be 
visible.
+            key = 
EncryptedKeyProcessor.prepareSecretKeyFromEncryptedKey(symEncAlgo, symmKey);
         } else if (retrievalMethodElement != null
             && "http://www.w3.org/2001/04/xmlenc#EncryptedKey".equals(
                 retrievalMethodElement.getAttributeNS(null, "Type"))) {
@@ -137,7 +142,9 @@ public class EncryptedDataProcessor implements Processor {
             WSSecurityEngineResult result = data.getWsDocInfo().getResult(uri);
             if (result != null) {
                 byte[] symmKey = 
(byte[])result.get(WSSecurityEngineResult.TAG_SECRET);
-                key = KeyUtils.prepareSecretKey(symEncAlgo, symmKey);
+                //The RetrievalMethod states that this is an EncryptedKey, and 
the key it
+                //yielded need not be of the length this EncryptedData needs.
+                key = 
EncryptedKeyProcessor.prepareSecretKeyFromEncryptedKey(symEncAlgo, symmKey);
             }
         } else {
             throw new WSSecurityException(
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 c7d147f5c..3fba62952 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
@@ -516,6 +516,35 @@ public class EncryptedKeyProcessor implements Processor {
             || XMLCipher.SEED_128_KeyWrap.equals(transportAlgorithm);
     }
 
+    /**
+     * Prepare the symmetric key that an EncryptedKey yielded, without telling 
the sender
+     * whether it yielded anything.
+     *
+     * A key recovered from an EncryptedKey is the product of a private key 
operation on
+     * ciphertext the sender chose, and a decryption that fails produces a 
random key of
+     * whatever length the EncryptedKey's own ReferenceList implies - which 
need not be the
+     * length the EncryptedData being decrypted requires. Rejecting the 
message on that
+     * mismatch 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 sender can provoke at will by 
choosing the
+     * EncryptedData algorithm. Carry on with a random key of the right length 
instead, so
+     * that both outcomes are the same outcome: the data does not decrypt.
+     *
+     * This belongs only to a key that came from an EncryptedKey. A key taken 
from a token
+     * or supplied by the CallbackHandler - a Kerberos session key, say - 
carries no such
+     * signal, and a length that does not match is a configuration error 
there, so it is
+     * reported rather than hidden behind a decryption failure.
+     */
+    static SecretKey prepareSecretKeyFromEncryptedKey(String symEncAlgo, 
byte[] secretKey)
+        throws WSSecurityException {
+        try {
+            return KeyUtils.prepareSecretKey(symEncAlgo, secretKey);
+        } catch (WSSecurityException ex) {
+            LOG.debug("The key recovered from the EncryptedKey does not match 
{}", symEncAlgo);
+            return KeyUtils.prepareSecretKey(symEncAlgo, 
getRandomKey(symEncAlgo));
+        }
+    }
+
     /**
      * Generates a random secret key using the algorithm specified in the
      * first DataReference URI
@@ -764,22 +793,11 @@ public class EncryptedKeyProcessor implements Processor {
 
         SecretKey symmetricKey = null;
         try {
-            symmetricKey = KeyUtils.prepareSecretKey(symEncAlgo, 
decryptedData);
+            symmetricKey = prepareSecretKeyFromEncryptedKey(symEncAlgo, 
decryptedData);
         } catch (IllegalArgumentException ex) {
             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/main/java/org/apache/wss4j/dom/processor/ReferenceListProcessor.java
 
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/ReferenceListProcessor.java
index aa062bfac..a431205bb 100644
--- 
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/ReferenceListProcessor.java
+++ 
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/ReferenceListProcessor.java
@@ -167,7 +167,9 @@ public class ReferenceListProcessor implements Processor {
             STRParserResult parserResult = 
strParser.parseSecurityTokenReference(parameters);
             byte[] secretKey = parserResult.getSecretKey();
             principal = parserResult.getPrincipal();
-            symmetricKey = KeyUtils.prepareSecretKey(symEncAlgo, secretKey);
+            symmetricKey = parserResult.isSecretKeyFromEncryptedKey()
+                ? 
EncryptedKeyProcessor.prepareSecretKeyFromEncryptedKey(symEncAlgo, secretKey)
+                : KeyUtils.prepareSecretKey(symEncAlgo, secretKey);
         }
 
         // Check for compliance against the defined AlgorithmSuite
diff --git 
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/str/STRParserResult.java 
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/str/STRParserResult.java
index 942b56bc6..9ad3f0c56 100644
--- 
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/str/STRParserResult.java
+++ 
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/str/STRParserResult.java
@@ -40,6 +40,8 @@ public class STRParserResult {
 
     private boolean trustedCredential;
 
+    private boolean secretKeyFromEncryptedKey;
+
     private REFERENCE_TYPE referenceType;
 
     /**
@@ -89,6 +91,17 @@ public class STRParserResult {
         return trustedCredential;
     }
 
+    /**
+     * Get whether the secret key was recovered from an EncryptedKey, rather 
than taken from a
+     * token or supplied by the CallbackHandler. Such a key is the product of 
a private key
+     * operation on ciphertext the sender chose, so a caller must not let the 
sender tell from
+     * the response whether that operation succeeded.
+     * @return true if the secret key was recovered from an EncryptedKey
+     */
+    public boolean isSecretKeyFromEncryptedKey() {
+        return secretKeyFromEncryptedKey;
+    }
+
     /**
      * Get how the certificates were referenced
      * @return how the certificates were referenced
@@ -117,6 +130,10 @@ public class STRParserResult {
         this.trustedCredential = trustedCredential;
     }
 
+    public void setSecretKeyFromEncryptedKey(boolean 
secretKeyFromEncryptedKey) {
+        this.secretKeyFromEncryptedKey = secretKeyFromEncryptedKey;
+    }
+
     public void setReferenceType(REFERENCE_TYPE referenceType) {
         this.referenceType = referenceType;
     }
diff --git 
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/str/SecurityTokenRefSTRParser.java
 
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/str/SecurityTokenRefSTRParser.java
index a8d02ef9f..1ebc03240 100644
--- 
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/str/SecurityTokenRefSTRParser.java
+++ 
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/str/SecurityTokenRefSTRParser.java
@@ -122,6 +122,7 @@ public class SecurityTokenRefSTRParser implements STRParser 
{
             STRParserUtil.checkEncryptedKeyBSPCompliance(secRef, 
data.getBSPEnforcer());
             byte[] secretKey = 
(byte[])result.get(WSSecurityEngineResult.TAG_SECRET);
             parserResult.setSecretKey(secretKey);
+            parserResult.setSecretKeyFromEncryptedKey(true);
         } else if (action != null && WSConstants.DKT == action.intValue()) {
             DerivedKeyToken dkt =
                 
(DerivedKeyToken)result.get(WSSecurityEngineResult.TAG_DERIVED_KEY_TOKEN);
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
index 9e7407a68..5ad82d8e1 100644
--- 
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
@@ -29,6 +29,7 @@ import javax.crypto.KeyGenerator;
 import javax.crypto.SecretKey;
 import javax.security.auth.callback.CallbackHandler;
 
+import org.apache.wss4j.common.WSEncryptionPart;
 import org.apache.wss4j.common.crypto.Crypto;
 import org.apache.wss4j.common.crypto.CryptoFactory;
 import org.apache.wss4j.common.crypto.CryptoType;
@@ -121,6 +122,68 @@ public class EncryptedKeyLengthOracleTest {
         assertEquals(failedDecryption.getMessage(), overlong.getMessage());
     }
 
+    /**
+     * The same property where the EncryptedKey is embedded in the KeyInfo of 
the EncryptedData
+     * it keys, rather than standing alone in the security header. The key is 
then prepared by
+     * EncryptedDataProcessor rather than by the EncryptedKeyProcessor that 
produced it, and an
+     * embedded EncryptedKey has no ReferenceList of its own to say what 
length a random
+     * replacement key should be - so the two failures part company there 
unless that path takes
+     * the same care.
+     */
+    @Test
+    public void 
testWrongLengthPlaintextIsIndistinguishableForAnEmbeddedEncryptedKey() throws 
Exception {
+        assumeFalse(isIBMJdK);
+
+        WSSecurityException failedDecryption =
+            
decryptWithEmbeddedEncryptedKey(forgeCiphertext(NON_CONFORMING_BLOCK_TYPE, 16));
+
+        WSSecurityException wrongLength =
+            decryptWithEmbeddedEncryptedKey(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");
+    }
+
+    /**
+     * Build a message whose security header holds an EncryptedData - the 
encrypted Timestamp -
+     * that carries its EncryptedKey inline in its own KeyInfo, substitute the 
given bytes for
+     * that EncryptedKey's CipherValue, and process it.
+     */
+    private WSSecurityException decryptWithEmbeddedEncryptedKey(byte[] 
cipherValue) throws Exception {
+        Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
+        WSSecHeader secHeader = new WSSecHeader(doc);
+        secHeader.insertSecurityHeader();
+
+        WSSecTimestamp timestamp = new WSSecTimestamp(secHeader);
+        timestamp.setTimeToLive(300);
+        timestamp.build();
+
+        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();
+        builder.prepare(crypto, symmetricKey);
+        builder.setEmbedEncryptedKey(true);
+        builder.prependBSTElementToHeader();
+
+        // Encrypting the Timestamp leaves the EncryptedData in the security 
header, where the
+        // engine reaches it directly. The ReferenceList that encrypt() 
returns is discarded, so
+        // the embedded EncryptedKey has none.
+        builder.getParts().add(new WSEncryptionPart("Timestamp", 
WSConstants.WSU_NS, ""));
+        builder.encrypt(symmetricKey);
+
+        substituteCipherValue(doc, cipherValue);
+        return processExpectingFailure(doc);
+    }
+
     /**
      * 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
@@ -141,15 +204,26 @@ public class EncryptedKeyLengthOracleTest {
         SecretKey symmetricKey = keyGen.generateKey();
         Document encryptedDoc = builder.build(crypto, symmetricKey);
 
+        substituteCipherValue(encryptedDoc, cipherValue);
+        return processExpectingFailure(encryptedDoc);
+    }
+
+    /**
+     * Replace the CipherValue of the message's EncryptedKey, wherever it 
sits, with the given
+     * bytes.
+     */
+    private void substituteCipherValue(Document doc, byte[] cipherValue) {
         Element encryptedKey =
-            XMLUtils.findElement(encryptedDoc.getDocumentElement(), 
"EncryptedKey", WSConstants.ENC_NS);
+            XMLUtils.findElement(doc.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));
+    }
 
+    private WSSecurityException processExpectingFailure(Document doc) {
         RequestData data = new RequestData();
         data.setDecCrypto(crypto);
         data.setSigVerCrypto(crypto);
@@ -158,7 +232,7 @@ public class EncryptedKeyLengthOracleTest {
 
         WSSecurityEngine secEngine = new WSSecurityEngine();
         return assertThrows(WSSecurityException.class,
-            () -> secEngine.processSecurityHeader(encryptedDoc, data));
+            () -> secEngine.processSecurityHeader(doc, data));
     }
 
     /**

Reply via email to