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)); } /**
