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 a235934ca Extend the EncryptedKey random-key fallback beyond
decryptDataRef (#719)
a235934ca is described below
commit a235934ca438328e10ee8910703eea916a5c3361
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Sep 21 10:11:46 2026 +0100
Extend the EncryptedKey random-key fallback beyond decryptDataRef (#719)
---
.../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 +++++++++++++++++++++-
6 files changed, 137 insertions(+), 18 deletions(-)
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));
}
/**