F-5798: reject non-byte-aligned AES-GCM/CCM tag lengths at Cipher init

pull/248/head
Chris Conlon 2026-07-24 11:59:10 -06:00
parent f1a011956e
commit 97842c5087
2 changed files with 151 additions and 14 deletions

View File

@ -793,6 +793,33 @@ public class WolfCryptCipher extends CipherSpi {
}
}
/**
* Convert an AEAD tag length from bits to bytes.
*
* Rejects lengths that are not a whole number of bytes, since the byte
* count is what reaches native wolfSSL and a non-multiple of 8 would be
* silently truncated. Native validates which sizes the mode supports.
*
* @param modeName mode label used in the error message
* @param tagBits requested tag length in bits
*
* @return tag length in bytes
*
* @throws InvalidAlgorithmParameterException if tagBits is not a
* positive multiple of 8
*/
private static int tagLenToBytes(String modeName, int tagBits)
throws InvalidAlgorithmParameterException {
if (tagBits <= 0 || (tagBits % 8) != 0) {
throw new InvalidAlgorithmParameterException(
modeName + " tag length must be a positive multiple of " +
"8 bits, got " + tagBits);
}
return (tagBits / 8);
}
private void wolfCryptSetIV(AlgorithmParameterSpec spec,
SecureRandom random) throws InvalidAlgorithmParameterException {
@ -853,13 +880,7 @@ public class WolfCryptCipher extends CipherSpi {
}
this.iv = gcmSpec.getIV().clone();
/* store tag length as bytes */
if (gcmSpec.getTLen() == 0) {
throw new InvalidAlgorithmParameterException(
"Tag length cannot be zero");
}
this.gcmTagLen = (gcmSpec.getTLen() / 8);
this.gcmTagLen = tagLenToBytes("AES-GCM", gcmSpec.getTLen());
}
else if (cipherMode == CipherMode.WC_CCM) {
/*
@ -891,13 +912,7 @@ public class WolfCryptCipher extends CipherSpi {
}
this.iv = ccmSpec.getIV().clone();
/* store tag length as bytes */
if (ccmSpec.getTLen() == 0) {
throw new InvalidAlgorithmParameterException(
"Tag length cannot be zero");
}
this.gcmTagLen = (ccmSpec.getTLen() / 8);
this.gcmTagLen = tagLenToBytes("AES-CCM", ccmSpec.getTLen());
}
else {
if (!(spec instanceof IvParameterSpec)) {

View File

@ -84,6 +84,7 @@ import com.wolfssl.wolfcrypt.FeatureDetect;
import com.wolfssl.wolfcrypt.Aes;
import com.wolfssl.wolfcrypt.Fips;
import com.wolfssl.provider.jce.WolfCryptProvider;
import java.security.GeneralSecurityException;
import com.wolfssl.wolfcrypt.WolfCryptException;
import com.wolfssl.wolfcrypt.test.TimedTestWatcher;
@ -5772,6 +5773,127 @@ public class WolfCryptCipherTest {
}
}
@Test
public void testAesGcmTagLengthValidation() throws Exception {
if (!enabledJCEAlgos.contains("AES/GCM/NoPadding")) {
return;
}
byte[] key = new byte[16];
byte[] iv = new byte[12];
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
int[] truncatingTagBits = { 4, 100, 129 };
for (int tagBits : truncatingTagBits) {
Cipher cipher =
Cipher.getInstance("AES/GCM/NoPadding", jceProvider);
try {
cipher.init(Cipher.ENCRYPT_MODE, keySpec,
new GCMParameterSpec(tagBits, iv));
fail("Should reject GCM tag length " + tagBits + " bits");
} catch (InvalidAlgorithmParameterException e) {
assertTrue("Error should mention tag length",
e.getMessage().contains("tag length"));
}
}
/* Whole byte counts, so rejected below the JCE layer rather than at
* init. Only sizes over the AES block size are rejected on every
* build, minimum tag size enforcement varies by configuration. */
int[] unsupportedTagBits = { 256 };
for (int tagBits : unsupportedTagBits) {
Cipher cipher =
Cipher.getInstance("AES/GCM/NoPadding", jceProvider);
cipher.init(Cipher.ENCRYPT_MODE, keySpec,
new GCMParameterSpec(tagBits, iv));
try {
cipher.doFinal("test".getBytes());
fail("Should reject GCM tag length " + tagBits + " bits");
} catch (WolfCryptException | GeneralSecurityException e) {
/* expected */
}
}
byte[] plaintext = "GCM tag length test".getBytes();
int[] goodTagBits = { 96, 104, 112, 120, 128 };
for (int tagBits : goodTagBits) {
secureRandom.nextBytes(iv);
Cipher enc =
Cipher.getInstance("AES/GCM/NoPadding", jceProvider);
enc.init(Cipher.ENCRYPT_MODE, keySpec,
new GCMParameterSpec(tagBits, iv));
byte[] ct = enc.doFinal(plaintext);
Cipher dec =
Cipher.getInstance("AES/GCM/NoPadding", jceProvider);
dec.init(Cipher.DECRYPT_MODE, keySpec,
new GCMParameterSpec(tagBits, iv));
assertArrayEquals(plaintext, dec.doFinal(ct));
}
}
@Test
public void testAesCcmTagLengthValidation() throws Exception {
if (!enabledJCEAlgos.contains("AES/CCM/NoPadding")) {
return;
}
byte[] key = new byte[16];
byte[] nonce = new byte[12];
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
int[] truncatingTagBits = { 4, 100, 129 };
for (int tagBits : truncatingTagBits) {
Cipher cipher =
Cipher.getInstance("AES/CCM/NoPadding", jceProvider);
try {
cipher.init(Cipher.ENCRYPT_MODE, keySpec,
new GCMParameterSpec(tagBits, nonce));
fail("Should reject CCM tag length " + tagBits + " bits");
} catch (InvalidAlgorithmParameterException e) {
assertTrue("Error should mention tag length",
e.getMessage().contains("tag length"));
}
}
/* Whole byte counts, so rejected below the JCE layer rather than at
* init. Only sizes over the AES block size are rejected on every
* build, the RFC 3610 set is not enforced in all FIPS bundles. */
int[] unsupportedTagBits = { 256 };
for (int tagBits : unsupportedTagBits) {
Cipher cipher =
Cipher.getInstance("AES/CCM/NoPadding", jceProvider);
cipher.init(Cipher.ENCRYPT_MODE, keySpec,
new GCMParameterSpec(tagBits, nonce));
try {
cipher.doFinal("test".getBytes());
fail("Should reject CCM tag length " + tagBits + " bits");
} catch (WolfCryptException | GeneralSecurityException e) {
/* expected */
}
}
/* 32-bit tags are valid for CCM, unlike GCM */
byte[] plaintext = "CCM tag length test".getBytes();
int[] goodTagBits = { 32, 64, 96, 128 };
for (int tagBits : goodTagBits) {
secureRandom.nextBytes(nonce);
Cipher enc =
Cipher.getInstance("AES/CCM/NoPadding", jceProvider);
enc.init(Cipher.ENCRYPT_MODE, keySpec,
new GCMParameterSpec(tagBits, nonce));
byte[] ct = enc.doFinal(plaintext);
Cipher dec =
Cipher.getInstance("AES/CCM/NoPadding", jceProvider);
dec.init(Cipher.DECRYPT_MODE, keySpec,
new GCMParameterSpec(tagBits, nonce));
assertArrayEquals(plaintext, dec.doFinal(ct));
}
}
/**
* Test IV consistency across encryption/decryption cycles.
* This test prevents regression of the issue where getIV() would return