-
Notifications
You must be signed in to change notification settings - Fork 298
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding test vectors for RSA PKCS #1 v 1.5.
The test vectors replace the previous test for padding oracles, which had to generate a new key. The test vectors add some new test cases that are edge cases for the modular exponentiation. NOKEYCHECK=True PiperOrigin-RevId: 236813461 GitOrigin-RevId: a92ab2a6af48d3285735fbf5c8ecb66c0750f46b
- Loading branch information
Showing
5 changed files
with
4,343 additions
and
135 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,14 +13,17 @@ | |
*/ | ||
package com.google.security.wycheproof; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
import static org.junit.Assert.fail; | ||
|
||
import java.security.KeyPair; | ||
import java.security.KeyPairGenerator; | ||
import com.google.gson.JsonElement; | ||
import com.google.gson.JsonObject; | ||
import java.security.KeyFactory; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.SecureRandom; | ||
import java.util.Arrays; | ||
import java.util.HashSet; | ||
import java.security.PrivateKey; | ||
import java.security.spec.PKCS8EncodedKeySpec; | ||
import java.util.Set; | ||
import java.util.TreeSet; | ||
import javax.crypto.Cipher; | ||
import javax.crypto.NoSuchPaddingException; | ||
import org.junit.Test; | ||
|
@@ -32,11 +35,6 @@ | |
* | ||
* @author [email protected] (Daniel Bleichenbacher) | ||
*/ | ||
// TODO(bleichen): test vectors check special cases: | ||
// - ciphertext too long | ||
// - plaintext too long | ||
// - ciphertext 0 | ||
// - ciphertext == modulus timing attacks | ||
@RunWith(JUnit4.class) | ||
public class RsaEncryptionTest { | ||
|
||
|
@@ -61,8 +59,34 @@ public void testOutdatedProvider() throws Exception { | |
} | ||
|
||
/** | ||
* Tries decrypting random messages with a given algorithm. Counts the number of distinct error | ||
* messages and expects this number to be 1. | ||
* Get a PublicKey from a JsonObject. | ||
* | ||
* <p>object contains the key in multiple formats: "key" : elements of the public key "keyDer": | ||
* the key in ASN encoding encoded hexadecimal "keyPem": the key in Pem format encoded hexadecimal | ||
* The test can use the format that is most convenient. | ||
*/ | ||
// This is a false positive, since errorprone cannot track values passed into a method. | ||
@SuppressWarnings("InsecureCryptoUsage") | ||
protected static PrivateKey getPrivateKey(JsonObject object) throws Exception { | ||
KeyFactory kf; | ||
kf = KeyFactory.getInstance("RSA"); | ||
byte[] encoded = TestUtil.hexToBytes(object.get("privateKeyPkcs8").getAsString()); | ||
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); | ||
return kf.generatePrivate(keySpec); | ||
} | ||
|
||
/** Convenience method to get a byte array from a JsonObject */ | ||
protected static byte[] getBytes(JsonObject object, String name) throws Exception { | ||
return JsonUtil.asByteArray(object.get(name)); | ||
} | ||
|
||
/** | ||
* Tries decrypting RSA-PKCS #1 v 1.5 encrypted ciphertext. | ||
* RSA-PKCS #1 v 1.5 is susceptible to chosen ciphertext attacks. The seriousness of the | ||
* attack depends on how much information is leaked when decrypting an invalid ciphertext. | ||
* The test vectors with invalid padding contain a flag "InvalidPkcs1Padding". | ||
* The test below expects that all test vectors with this flag throw an indistinguishable | ||
* exception. | ||
* | ||
* <p><b>References:</b> | ||
* | ||
|
@@ -99,143 +123,103 @@ public void testOutdatedProvider() throws Exception { | |
* </ul> | ||
*/ | ||
@SuppressWarnings("InsecureCryptoUsage") | ||
public void testExceptions(String algorithm) throws Exception { | ||
KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA"); | ||
keygen.initialize(1024); | ||
KeyPair keypair = keygen.genKeyPair(); | ||
SecureRandom rand = new SecureRandom(); | ||
Cipher c = Cipher.getInstance(algorithm); | ||
byte[] ciphertext = new byte[1024 / 8]; | ||
HashSet<String> exceptions = new HashSet<String>(); | ||
final int samples = 1000; | ||
for (int i = 0; i < samples; i++) { | ||
rand.nextBytes(ciphertext); | ||
ciphertext[0] &= (byte) 0x7f; | ||
try { | ||
c.init(Cipher.DECRYPT_MODE, keypair.getPrivate()); | ||
c.doFinal(ciphertext); | ||
} catch (Exception ex) { | ||
exceptions.add(ex.toString()); | ||
} | ||
public void testDecryption(String filename) throws Exception { | ||
final String expectedSchema = "rsaes_pkcs1_decrypt_schema.json"; | ||
JsonObject test = JsonUtil.getTestVectors(filename); | ||
String schema = test.get("schema").getAsString(); | ||
if (!schema.equals(expectedSchema)) { | ||
System.out.println( | ||
"Expecting test vectors with schema " | ||
+ expectedSchema | ||
+ " found vectors with schema " | ||
+ schema); | ||
} | ||
Cipher enc = Cipher.getInstance("RSA/ECB/NOPADDING"); | ||
byte[][] paddedKeys = generatePkcs1Vectors(1024 / 8); | ||
for (int i = 0; i < paddedKeys.length; i++) { | ||
enc.init(Cipher.ENCRYPT_MODE, keypair.getPublic()); | ||
ciphertext = enc.doFinal(paddedKeys[i]); | ||
try { | ||
c.init(Cipher.DECRYPT_MODE, keypair.getPrivate()); | ||
c.doFinal(ciphertext); | ||
} catch (Exception ex) { | ||
exceptions.add(ex.toString()); | ||
// Padding oracle attacks become simpler when the decryption leaks detailed information about | ||
// invalid paddings. Hence implementations are expected to not include such information in the | ||
// exception thrown in the case of an invalid padding. | ||
// Test vectors with an invalid padding have a flag "InvalidPkcs1Padding". | ||
// Invalid test vectors without this flag are cases where the error are detected before | ||
// the ciphertext is decrypted, e.g. if the size of the ciphertext is incorrect. | ||
final String invalidPkcs1Padding = "InvalidPkcs1Padding"; | ||
Set<String> exceptions = new TreeSet<String>(); | ||
|
||
int errors = 0; | ||
Cipher decrypter = Cipher.getInstance("RSA/ECB/PKCS1Padding"); | ||
for (JsonElement g : test.getAsJsonArray("testGroups")) { | ||
JsonObject group = g.getAsJsonObject(); | ||
PrivateKey key = getPrivateKey(group); | ||
for (JsonElement t : group.getAsJsonArray("tests")) { | ||
JsonObject testcase = t.getAsJsonObject(); | ||
int tcid = testcase.get("tcId").getAsInt(); | ||
String messageHex = TestUtil.bytesToHex(getBytes(testcase, "msg")); | ||
byte[] ciphertext = getBytes(testcase, "ct"); | ||
String ciphertextHex = TestUtil.bytesToHex(ciphertext); | ||
String result = testcase.get("result").getAsString(); | ||
decrypter.init(Cipher.DECRYPT_MODE, key); | ||
byte[] decrypted = null; | ||
String exception = ""; | ||
try { | ||
decrypted = decrypter.doFinal(ciphertext); | ||
} catch (Exception ex) { | ||
// TODO(bleichen): The exception thrown should always be | ||
// a GeneralSecurityException. | ||
// However, BouncyCastle throws some non-conforming exceptions. | ||
// For the moment we do not count this as a problem to avoid that | ||
// more serious bugs remain hidden. In particular, the test expects | ||
// that all ciphertexts with an invalid padding throw the same | ||
// indistinguishable exception. | ||
decrypted = null; | ||
exception = ex.toString(); | ||
for (JsonElement flag : testcase.getAsJsonArray("flags")) { | ||
if (flag.getAsString().equals(invalidPkcs1Padding)) { | ||
exceptions.add(exception); | ||
break; | ||
} | ||
} | ||
} | ||
if (decrypted == null && result.equals("valid")) { | ||
System.out.printf( | ||
"Valid ciphertext not decrypted. filename:%s tcId:%d ct:%s cause:%s\n", | ||
filename, tcid, ciphertextHex, exception); | ||
errors++; | ||
} else if (decrypted != null) { | ||
String decryptedHex = TestUtil.bytesToHex(decrypted); | ||
if (result.equals("invalid")) { | ||
System.out.printf( | ||
"Invalid ciphertext decrypted. filename:%s tcId:%d expected:%s decrypted:%s\n", | ||
filename, tcid, messageHex, decryptedHex); | ||
errors++; | ||
} else if (!decryptedHex.equals(messageHex)) { | ||
System.out.printf( | ||
"Incorrect decryption. filename:%s tcId:%d expected:%s decrypted:%s\n", | ||
filename, tcid, messageHex, decryptedHex); | ||
errors++; | ||
} | ||
} | ||
} | ||
} | ||
if (exceptions.size() > 1) { | ||
System.out.println("Exceptions for " + algorithm); | ||
if (exceptions.size() != 1) { | ||
System.out.println("Exceptions for RSA/ECB/PKCS1Padding"); | ||
for (String s : exceptions) { | ||
System.out.println(s); | ||
} | ||
fail("Exceptions leak information about the padding for " + algorithm); | ||
fail("Exceptions leak information about the padding"); | ||
} | ||
assertEquals(0, errors); | ||
} | ||
|
||
/** | ||
* Tests the exceptions for RSA decryption with PKCS1Padding. PKCS1Padding is susceptible to | ||
* chosen message attacks. Nonetheless, to minimize the damage of such an attack an implementation | ||
* should minimize the information about the failure in the padding. | ||
*/ | ||
@Test | ||
public void testExceptionsPKCS1() throws Exception { | ||
testExceptions("RSA/ECB/PKCS1PADDING"); | ||
public void testDecryption2048() throws Exception { | ||
testDecryption("rsa_pkcs1_2048_test.json"); | ||
} | ||
|
||
@Test | ||
public void testGetExceptionsOAEP() throws Exception { | ||
testExceptions("RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING"); | ||
} | ||
|
||
/** | ||
* Generates PKCS#1 invalid vectors | ||
* | ||
* @param rsaKeyLength | ||
*/ | ||
private byte[][] generatePkcs1Vectors(int rsaKeyLength) { | ||
// create plain padded keys | ||
byte[][] plainPaddedKeys = new byte[13][]; | ||
// no 0x00 byte to deliver a symmetric key | ||
plainPaddedKeys[0] = getEK_NoNullByte(rsaKeyLength); | ||
// 0x00 too early in the padding | ||
plainPaddedKeys[1] = getEK_NullByteInPadding(rsaKeyLength); | ||
// 0x00 too early in the PKCS#1 padding | ||
plainPaddedKeys[2] = getEK_NullByteInPkcsPadding(rsaKeyLength); | ||
// decrypted ciphertext starting with 0x17 0x02 | ||
plainPaddedKeys[3] = getEK_WrongFirstByte(rsaKeyLength); | ||
// decrypted ciphertext starting with 0x00 0x17 | ||
plainPaddedKeys[4] = getEK_WrongSecondByte(rsaKeyLength); | ||
// different lengths of the decrypted unpadded key | ||
plainPaddedKeys[5] = getPaddedKey(rsaKeyLength, 0); | ||
plainPaddedKeys[6] = getPaddedKey(rsaKeyLength, 1); | ||
plainPaddedKeys[7] = getPaddedKey(rsaKeyLength, 8); | ||
plainPaddedKeys[8] = getPaddedKey(rsaKeyLength, 16); | ||
plainPaddedKeys[9] = getPaddedKey(rsaKeyLength, 96); | ||
// the decrypted padded plaintext is shorter than RSA key | ||
plainPaddedKeys[10] = zeroPad(getPaddedKey(rsaKeyLength - 1, 16), rsaKeyLength); | ||
plainPaddedKeys[11] = zeroPad(getPaddedKey(rsaKeyLength - 2, 16), rsaKeyLength); | ||
// just 0x00 bytes | ||
plainPaddedKeys[12] = new byte[rsaKeyLength]; | ||
return plainPaddedKeys; | ||
} | ||
|
||
/** | ||
* Prepends data with some zeros, so that the result is keyLength bytes long. | ||
*/ | ||
private byte[] zeroPad(byte[] data, int keyLength) { | ||
byte[] padded = new byte[keyLength]; | ||
System.arraycopy(data, 0, padded, keyLength - data.length, data.length); | ||
return padded; | ||
public void testDecryption3072() throws Exception { | ||
testDecryption("rsa_pkcs1_3072_test.json"); | ||
} | ||
|
||
private byte[] getPaddedKey(int rsaKeyLength, int symmetricKeyLength) { | ||
byte[] key = new byte[rsaKeyLength]; | ||
// fill all the bytes with non-zero values | ||
Arrays.fill(key, (byte) 42); | ||
// set the first byte to 0x00 | ||
key[0] = 0x00; | ||
// set the second byte to 0x02 | ||
key[1] = 0x02; | ||
// set the separating byte | ||
if (symmetricKeyLength != -1) { | ||
key[rsaKeyLength - symmetricKeyLength - 1] = 0x00; | ||
} | ||
return key; | ||
} | ||
|
||
private byte[] getEK_WrongFirstByte(int rsaKeyLength) { | ||
byte[] key = getPaddedKey(rsaKeyLength, 16); | ||
key[0] = 23; | ||
return key; | ||
} | ||
|
||
private byte[] getEK_WrongSecondByte(int rsaKeyLength) { | ||
byte[] key = getPaddedKey(rsaKeyLength, 16); | ||
key[1] = 23; | ||
return key; | ||
} | ||
|
||
private byte[] getEK_NoNullByte(int rsaKeyLength) { | ||
byte[] key = getPaddedKey(rsaKeyLength, -1); | ||
return key; | ||
} | ||
|
||
private byte[] getEK_NullByteInPkcsPadding(int rsaKeyLength) { | ||
byte[] key = getPaddedKey(rsaKeyLength, 16); | ||
key[3] = 0x00; | ||
return key; | ||
} | ||
|
||
private byte[] getEK_NullByteInPadding(int rsaKeyLength) { | ||
byte[] key = getPaddedKey(rsaKeyLength, 16); | ||
key[11] = 0x00; | ||
return key; | ||
@Test | ||
public void testDecryption4096() throws Exception { | ||
testDecryption("rsa_pkcs1_4096_test.json"); | ||
} | ||
} |
Oops, something went wrong.