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
This commit is contained in:
@@ -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 @@ import org.junit.runners.JUnit4;
|
||||
*
|
||||
* @author bleichen@google.com (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 class RsaEncryptionTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 class RsaEncryptionTest {
|
||||
* </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);
|
||||
}
|
||||
// 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
assertEquals(0, errors);
|
||||
}
|
||||
|
||||
@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;
|
||||
public void testDecryption2048() throws Exception {
|
||||
testDecryption("rsa_pkcs1_2048_test.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@Test
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user