Skip to content

Commit

Permalink
Adding test vectors for RSA PKCS #1 v 1.5.
Browse files Browse the repository at this point in the history
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
bleichen authored and Tink Team committed Nov 26, 2019
1 parent 4cd1995 commit 3255146
Show file tree
Hide file tree
Showing 5 changed files with 4,343 additions and 135 deletions.
254 changes: 119 additions & 135 deletions java/com/google/security/wycheproof/testcases/RsaEncryptionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand All @@ -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>
*
Expand Down Expand Up @@ -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");
}
}
Loading

0 comments on commit 3255146

Please sign in to comment.