github-advanced-security[bot] commented on code in PR #709: URL: https://github.com/apache/ws-wss4j/pull/709#discussion_r4047615182
########## 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"); Review Comment: ## CodeQL / Use of RSA algorithm without OAEP This specification is used to [initialize an RSA cipher](1) without OAEP padding. [Show more details](https://github.com/apache/ws-wss4j/security/code-scanning/423) -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
