test(applet): adversarial GCM regression tests (post-M2 security pass)
Pin five negative-space properties of AliroGcm.decrypt that the NIST KAT suite doesn't exercise. Catches the implementation flaws most likely to slip into a userland AEAD impl: - decrypt_tamperedCiphertextByte: single bit flip in ct rejects + out[] unchanged (proves tag verify runs BEFORE GCTR-decrypt) - decrypt_tamperedTagByte: single bit flip in tag rejects + out[] unchanged (proves no early-exit / no conditional plaintext leak) - decrypt_wrongKey: single-bit key difference rejects (key confusion) - decrypt_wrongIv: single-bit IV difference rejects (replay-with-rebind) - decrypt_inputShorterThanTag: <16 B input throws ILLEGAL_VALUE without touching out[] (early bounds check) 13/13 AliroGcmTest tests pass (was 8 + 5 new). Full suite 152/0/0.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package com.dangerousthings.aliro;
|
package com.dangerousthings.aliro;
|
||||||
|
|
||||||
import javacard.security.AESKey;
|
import javacard.security.AESKey;
|
||||||
|
import javacard.security.CryptoException;
|
||||||
import javacard.security.KeyBuilder;
|
import javacard.security.KeyBuilder;
|
||||||
import javacardx.crypto.AEADCipher;
|
import javacardx.crypto.AEADCipher;
|
||||||
import javacardx.crypto.Cipher;
|
import javacardx.crypto.Cipher;
|
||||||
@@ -9,6 +10,8 @@ import org.junit.jupiter.api.Test;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link AliroGcm} — our userland AES-256-GCM, built on the JC
|
* Tests for {@link AliroGcm} — our userland AES-256-GCM, built on the JC
|
||||||
@@ -277,6 +280,148 @@ class AliroGcmTest {
|
|||||||
"different IVs must produce different ciphertexts (CTR stream differs)");
|
"different IVs must produce different ciphertexts (CTR stream differs)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Adversarial regression tests (post-M2 security pass) ---------------
|
||||||
|
//
|
||||||
|
// These pin the documented defensive properties of AliroGcm.decrypt:
|
||||||
|
// 1. Any single-byte mutation in the ciphertext rejects (forgery).
|
||||||
|
// 2. Any single-byte mutation in the tag rejects (forgery).
|
||||||
|
// 3. Wrong key under same (IV, ct, tag) rejects (key confusion).
|
||||||
|
// 4. Wrong IV under same (key, ct, tag) rejects (replay-with-rebind).
|
||||||
|
// 5. Truncated input (< TAG_LEN) rejects without touching the output.
|
||||||
|
//
|
||||||
|
// None of these are NIST KAT cases; they are negative-space coverage for
|
||||||
|
// implementation flaws like early-exit tag compare, conditional writes
|
||||||
|
// before tag verify, or missing bounds checks.
|
||||||
|
|
||||||
|
/** Encrypt a fixed plaintext under fixed (key, IV) and return ct||tag. */
|
||||||
|
private static byte[] freshCiphertext(byte[] key, byte[] iv, byte[] pt) {
|
||||||
|
byte[] out = new byte[pt.length + 16];
|
||||||
|
AliroGcm gcm = new AliroGcm();
|
||||||
|
gcm.encrypt(key, (short) 0, iv, (short) 0,
|
||||||
|
pt, (short) 0, (short) pt.length,
|
||||||
|
out, (short) 0);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void decrypt_tamperedCiphertextByte_throwsAndOutputUntouched() {
|
||||||
|
byte[] key = new byte[32];
|
||||||
|
for (int i = 0; i < 32; i++) key[i] = (byte) (0x10 + i);
|
||||||
|
byte[] iv = new byte[12];
|
||||||
|
for (int i = 0; i < 12; i++) iv[i] = (byte) (0xA0 + i);
|
||||||
|
byte[] pt = hex("00112233445566778899aabbccddeeff" + "0102030405");
|
||||||
|
|
||||||
|
byte[] ctTag = freshCiphertext(key, iv, pt);
|
||||||
|
// Flip one bit in the ciphertext (NOT the tag).
|
||||||
|
ctTag[7] ^= (byte) 0x01;
|
||||||
|
|
||||||
|
byte[] outSentinel = new byte[pt.length];
|
||||||
|
java.util.Arrays.fill(outSentinel, (byte) 0xCC);
|
||||||
|
|
||||||
|
AliroGcm gcm = new AliroGcm();
|
||||||
|
assertThrows(CryptoException.class, () -> gcm.decrypt(
|
||||||
|
key, (short) 0, iv, (short) 0,
|
||||||
|
ctTag, (short) 0, (short) ctTag.length,
|
||||||
|
outSentinel, (short) 0));
|
||||||
|
|
||||||
|
// Tag verify happens BEFORE GCTR-decrypt into out[]; output buffer
|
||||||
|
// must be byte-identical to what we filled before the call.
|
||||||
|
for (int i = 0; i < pt.length; i++) {
|
||||||
|
assertEquals((byte) 0xCC, outSentinel[i],
|
||||||
|
"tampered-ct decrypt must not write plaintext to out[" + i + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void decrypt_tamperedTagByte_throwsAndOutputUntouched() {
|
||||||
|
byte[] key = new byte[32];
|
||||||
|
for (int i = 0; i < 32; i++) key[i] = (byte) (0x20 + i);
|
||||||
|
byte[] iv = new byte[12];
|
||||||
|
for (int i = 0; i < 12; i++) iv[i] = (byte) (0xB0 + i);
|
||||||
|
byte[] pt = hex("deadbeefcafe1234");
|
||||||
|
|
||||||
|
byte[] ctTag = freshCiphertext(key, iv, pt);
|
||||||
|
// Flip one bit in the LAST byte of the 16-byte tag.
|
||||||
|
ctTag[ctTag.length - 1] ^= (byte) 0x80;
|
||||||
|
|
||||||
|
byte[] outSentinel = new byte[pt.length];
|
||||||
|
java.util.Arrays.fill(outSentinel, (byte) 0xDD);
|
||||||
|
|
||||||
|
AliroGcm gcm = new AliroGcm();
|
||||||
|
assertThrows(CryptoException.class, () -> gcm.decrypt(
|
||||||
|
key, (short) 0, iv, (short) 0,
|
||||||
|
ctTag, (short) 0, (short) ctTag.length,
|
||||||
|
outSentinel, (short) 0));
|
||||||
|
|
||||||
|
for (int i = 0; i < pt.length; i++) {
|
||||||
|
assertEquals((byte) 0xDD, outSentinel[i],
|
||||||
|
"tampered-tag decrypt must not write plaintext to out[" + i + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void decrypt_wrongKey_throws() {
|
||||||
|
byte[] key = new byte[32];
|
||||||
|
for (int i = 0; i < 32; i++) key[i] = (byte) (0x30 + i);
|
||||||
|
byte[] iv = new byte[12];
|
||||||
|
for (int i = 0; i < 12; i++) iv[i] = (byte) (0xC0 + i);
|
||||||
|
byte[] pt = hex("11223344");
|
||||||
|
|
||||||
|
byte[] ctTag = freshCiphertext(key, iv, pt);
|
||||||
|
|
||||||
|
byte[] wrongKey = key.clone();
|
||||||
|
wrongKey[5] ^= (byte) 0x01; // single-bit difference
|
||||||
|
|
||||||
|
byte[] out = new byte[pt.length];
|
||||||
|
AliroGcm gcm = new AliroGcm();
|
||||||
|
assertThrows(CryptoException.class, () -> gcm.decrypt(
|
||||||
|
wrongKey, (short) 0, iv, (short) 0,
|
||||||
|
ctTag, (short) 0, (short) ctTag.length,
|
||||||
|
out, (short) 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void decrypt_wrongIv_throws() {
|
||||||
|
byte[] key = new byte[32];
|
||||||
|
for (int i = 0; i < 32; i++) key[i] = (byte) (0x40 + i);
|
||||||
|
byte[] iv = new byte[12];
|
||||||
|
for (int i = 0; i < 12; i++) iv[i] = (byte) (0xD0 + i);
|
||||||
|
byte[] pt = hex("55667788");
|
||||||
|
|
||||||
|
byte[] ctTag = freshCiphertext(key, iv, pt);
|
||||||
|
|
||||||
|
byte[] wrongIv = iv.clone();
|
||||||
|
wrongIv[3] ^= (byte) 0x01;
|
||||||
|
|
||||||
|
byte[] out = new byte[pt.length];
|
||||||
|
AliroGcm gcm = new AliroGcm();
|
||||||
|
assertThrows(CryptoException.class, () -> gcm.decrypt(
|
||||||
|
key, (short) 0, wrongIv, (short) 0,
|
||||||
|
ctTag, (short) 0, (short) ctTag.length,
|
||||||
|
out, (short) 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void decrypt_inputShorterThanTag_throwsIllegalValueWithoutOutput() {
|
||||||
|
byte[] key = new byte[32];
|
||||||
|
byte[] iv = new byte[12];
|
||||||
|
// 15 bytes — one short of the minimum (16-byte tag with empty pt).
|
||||||
|
byte[] tooShort = new byte[15];
|
||||||
|
|
||||||
|
byte[] outSentinel = new byte[1];
|
||||||
|
outSentinel[0] = (byte) 0xEE;
|
||||||
|
|
||||||
|
AliroGcm gcm = new AliroGcm();
|
||||||
|
CryptoException ex = assertThrows(CryptoException.class, () -> gcm.decrypt(
|
||||||
|
key, (short) 0, iv, (short) 0,
|
||||||
|
tooShort, (short) 0, (short) tooShort.length,
|
||||||
|
outSentinel, (short) 0));
|
||||||
|
assertEquals(CryptoException.ILLEGAL_VALUE, ex.getReason(),
|
||||||
|
"input < TAG_LEN must surface CryptoException.ILLEGAL_VALUE");
|
||||||
|
assertEquals((byte) 0xEE, outSentinel[0],
|
||||||
|
"early reject must not touch the output buffer");
|
||||||
|
}
|
||||||
|
|
||||||
private static byte[] hex(String s) {
|
private static byte[] hex(String s) {
|
||||||
s = s.replaceAll("\\s+", "");
|
s = s.replaceAll("\\s+", "");
|
||||||
byte[] out = new byte[s.length() / 2];
|
byte[] out = new byte[s.length() / 2];
|
||||||
|
|||||||
Reference in New Issue
Block a user