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;
|
package com.google.security.wycheproof;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
import java.security.KeyPair;
|
import com.google.gson.JsonElement;
|
||||||
import java.security.KeyPairGenerator;
|
import com.google.gson.JsonObject;
|
||||||
|
import java.security.KeyFactory;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.PrivateKey;
|
||||||
import java.util.Arrays;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.util.HashSet;
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.NoSuchPaddingException;
|
import javax.crypto.NoSuchPaddingException;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -32,11 +35,6 @@ import org.junit.runners.JUnit4;
|
|||||||
*
|
*
|
||||||
* @author bleichen@google.com (Daniel Bleichenbacher)
|
* @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)
|
@RunWith(JUnit4.class)
|
||||||
public class RsaEncryptionTest {
|
public class RsaEncryptionTest {
|
||||||
|
|
||||||
@@ -61,8 +59,34 @@ public class RsaEncryptionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries decrypting random messages with a given algorithm. Counts the number of distinct error
|
* Get a PublicKey from a JsonObject.
|
||||||
* messages and expects this number to be 1.
|
*
|
||||||
|
* <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>
|
* <p><b>References:</b>
|
||||||
*
|
*
|
||||||
@@ -99,143 +123,103 @@ public class RsaEncryptionTest {
|
|||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("InsecureCryptoUsage")
|
@SuppressWarnings("InsecureCryptoUsage")
|
||||||
public void testExceptions(String algorithm) throws Exception {
|
public void testDecryption(String filename) throws Exception {
|
||||||
KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA");
|
final String expectedSchema = "rsaes_pkcs1_decrypt_schema.json";
|
||||||
keygen.initialize(1024);
|
JsonObject test = JsonUtil.getTestVectors(filename);
|
||||||
KeyPair keypair = keygen.genKeyPair();
|
String schema = test.get("schema").getAsString();
|
||||||
SecureRandom rand = new SecureRandom();
|
if (!schema.equals(expectedSchema)) {
|
||||||
Cipher c = Cipher.getInstance(algorithm);
|
System.out.println(
|
||||||
byte[] ciphertext = new byte[1024 / 8];
|
"Expecting test vectors with schema "
|
||||||
HashSet<String> exceptions = new HashSet<String>();
|
+ expectedSchema
|
||||||
final int samples = 1000;
|
+ " found vectors with schema "
|
||||||
for (int i = 0; i < samples; i++) {
|
+ schema);
|
||||||
rand.nextBytes(ciphertext);
|
}
|
||||||
ciphertext[0] &= (byte) 0x7f;
|
// Padding oracle attacks become simpler when the decryption leaks detailed information about
|
||||||
try {
|
// invalid paddings. Hence implementations are expected to not include such information in the
|
||||||
c.init(Cipher.DECRYPT_MODE, keypair.getPrivate());
|
// exception thrown in the case of an invalid padding.
|
||||||
c.doFinal(ciphertext);
|
// Test vectors with an invalid padding have a flag "InvalidPkcs1Padding".
|
||||||
} catch (Exception ex) {
|
// Invalid test vectors without this flag are cases where the error are detected before
|
||||||
exceptions.add(ex.toString());
|
// 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");
|
if (exceptions.size() != 1) {
|
||||||
byte[][] paddedKeys = generatePkcs1Vectors(1024 / 8);
|
System.out.println("Exceptions for RSA/ECB/PKCS1Padding");
|
||||||
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);
|
|
||||||
for (String s : exceptions) {
|
for (String s : exceptions) {
|
||||||
System.out.println(s);
|
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetExceptionsOAEP() throws Exception {
|
public void testDecryption2048() throws Exception {
|
||||||
testExceptions("RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING");
|
testDecryption("rsa_pkcs1_2048_test.json");
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Test
|
||||||
* Prepends data with some zeros, so that the result is keyLength bytes long.
|
public void testDecryption3072() throws Exception {
|
||||||
*/
|
testDecryption("rsa_pkcs1_3072_test.json");
|
||||||
private byte[] zeroPad(byte[] data, int keyLength) {
|
|
||||||
byte[] padded = new byte[keyLength];
|
|
||||||
System.arraycopy(data, 0, padded, keyLength - data.length, data.length);
|
|
||||||
return padded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] getPaddedKey(int rsaKeyLength, int symmetricKeyLength) {
|
@Test
|
||||||
byte[] key = new byte[rsaKeyLength];
|
public void testDecryption4096() throws Exception {
|
||||||
// fill all the bytes with non-zero values
|
testDecryption("rsa_pkcs1_4096_test.json");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"definitions": {
|
||||||
|
"RsaesPkcs1TestGroup": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"enum": [
|
||||||
|
"RsaesPkcs1Decrypt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"d": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "BigInt",
|
||||||
|
"description": "The private exponent"
|
||||||
|
},
|
||||||
|
"e": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "BigInt",
|
||||||
|
"description": "The public exponent"
|
||||||
|
},
|
||||||
|
"n": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "BigInt",
|
||||||
|
"description": "The modulus of the key"
|
||||||
|
},
|
||||||
|
"privateKeyJwk": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "JWK encoded private key"
|
||||||
|
},
|
||||||
|
"privateKeyPem": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "Pem",
|
||||||
|
"description": "Pem encoded private key"
|
||||||
|
},
|
||||||
|
"privateKeyPkcs8": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "Der",
|
||||||
|
"description": "Pkcs 8 encoded private key."
|
||||||
|
},
|
||||||
|
"tests": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/RsaesPkcs1TestVector"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RsaesPkcs1TestVector": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tcId": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Identifier of the test case"
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A brief description of the test case"
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "HexBytes",
|
||||||
|
"description": "The encrypted message"
|
||||||
|
},
|
||||||
|
"ct": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "HexBytes",
|
||||||
|
"description": "An encryption of msg"
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Test result",
|
||||||
|
"enum": [
|
||||||
|
"valid",
|
||||||
|
"invalid",
|
||||||
|
"acceptable"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"flags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "A list of flags"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"algorithm": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "the primitive tested in the test file"
|
||||||
|
},
|
||||||
|
"generatorVersion": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "the version of the test vectors."
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "additional documentation"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "a description of the labels used in the test vectors"
|
||||||
|
},
|
||||||
|
"numberOfTests": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "the number of test vectors in this test"
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"enum": [
|
||||||
|
"rsaes_pkcs1_decrypt_schema.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"testGroups": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/RsaesPkcs1TestGroup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user