CVE-2017-12972 (Java, published 2017-08-20T16:29Z)
Description: value: In Nimbus JOSE+JWT before 4.39, there is no integer-overflow check when converting length values from bytes to bits, which allows attackers to conduct HMAC bypass attacks by shifting Additional Authenticated Data (AAD) and ciphertext so that different plaintext is obtained for the same HMAC.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-345 \u2014 Insufficient Verification of Data Authenticity
The product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data.
Repository: https://bitbucket.org/connect2id/nimbus-jose-jwt
Files changed:
src/main/java/com/nimbusds/jose/crypto/AAD.java
src/main/java/com/nimbusds/jose/crypto/AESEncrypter.java
src/main/java/com/nimbusds/jose/crypto/AESGCM.java
src/main/java/com/nimbusds/jose/crypto/AESGCMKW.java
src/main/java/com/nimbusds/jose/crypto/ConcatKDF.java
src/main/java/com/nimbusds/jose/crypto/ContentCryptoProvider.java
src/main/java/com/nimbusds/jose/crypto/DirectEncrypter.java
src/main/java/com/nimbusds/jose/crypto/RSA1_5.java
src/main/java/com/nimbusds/jose/jwk/OctetSequenceKey.java
src/main/java/com/nimbusds/jose/jwk/RSAKey.java
src/main/java/com/nimbusds/jose/util/ByteUtils.java
src/main/java/com/nimbusds/jose/util/IntegerOverflowException.java
src/test/java/com/nimbusds/jose/crypto/AADTest.java
src/test/java/com/nimbusds/jose/crypto/AESCBCTest.java
src/test/java/com/nimbusds/jose/util/ByteUtilsTest.java
Commit message: Switches to safe bit computation in crypto (iss #224)


Vulnerable code:
 * @version 2015-05-14
	public static byte[] computeLength(final byte[] aad) {
		final int bitLength = ByteUtils.bitLength(aad);
import net.jcip.annotations.ThreadSafe;

 * @version 2016-10-13
			if(ByteUtils.bitLength(getKey().getEncoded()) != 128){
			if(ByteUtils.bitLength(getKey().getEncoded()) != 192){
			if (ByteUtils.bitLength(getKey().getEncoded()) != 256) {
			if(ByteUtils.bitLength(getKey().getEncoded()) != 128){
			if(ByteUtils.bitLength(getKey().getEncoded()) != 192){
			if(ByteUtils.bitLength(getKey().getEncoded()) != 256){

import net.jcip.annotations.ThreadSafe;

 * @version 2016-10-13
		if (ByteUtils.bitLength(iv) != IV_BIT_LENGTH) {
			throw new JOSEException(String.format("IV length of %d bits is required, got %d", IV_BIT_LENGTH, ByteUtils.bitLength(iv)));
 * @version 2016-10-13
		if (ByteUtils.bitLength(keyBytes) != keyLength) {
			throw new KeyLengthException("CEK key length mismatch: " + ByteUtils.bitLength(keyBytes) + " != " + keyLength);
 * @version 2017-05-05
		for (int i=1; i <= computeDigestCycles(ByteUtils.bitLength(md.getDigestLength()), keyLengthBits); i++) {
 * @version 2016-10-13
		if (enc.cekBitLength() != ByteUtils.bitLength(cek.getEncoded())) {
			throw new KeyLengthException("The Content Encryption Key (CEK) length for " + enc + " must be " + enc.cekBitLength() + " bits");
 * @version 2014-06-29
		if (enc.cekBitLength() != ByteUtils.bitLength(getKey().getEncoded())) {
 * @version 2016-12-04
			if (ByteUtils.bitLength(secretKeyBytes) != keyLength) {
import com.nimbusds.jose.util.Base64;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jose.util.ByteUtils;
import com.nimbusds.jose.util.JSONObjectUtils;
 * @version 2017-04-08
		return ByteUtils.bitLength(k.decode());
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jose.util.ByteUtils;
import com.nimbusds.jose.util.JSONObjectUtils;
 * @version 2017-04-08
		return ByteUtils.bitLength(n.decode());
 * @version 2015-05-12
import com.nimbusds.jose.util.Base64URL;
import junit.framework.TestCase;

 * @version 2015-05-17
	public void testComputeLength() {

 * @version 2017-05-30
	public void testAADLengthComputation() {

Fixed code:
import com.nimbusds.jose.util.IntegerOverflowException;
 * @version 2017-06-01
	 *
	 * @throws IntegerOverflowException On a integer overflow.
	public static byte[] computeLength(final byte[] aad)
		throws IntegerOverflowException {
		final int bitLength = ByteUtils.safeBitLength(aad);
import net.jcip.annotations.ThreadSafe;
 * @version 2017-06-01
			if(ByteUtils.safeBitLength(getKey().getEncoded()) != 128){
			if(ByteUtils.safeBitLength(getKey().getEncoded()) != 192){
			if (ByteUtils.safeBitLength(getKey().getEncoded()) != 256) {
			if(ByteUtils.safeBitLength(getKey().getEncoded()) != 128){
			if(ByteUtils.safeBitLength(getKey().getEncoded()) != 192){
			if(ByteUtils.safeBitLength(getKey().getEncoded()) != 256){
import net.jcip.annotations.ThreadSafe;
 * @version 2017-06-01
		if (ByteUtils.safeBitLength(iv) != IV_BIT_LENGTH) {
			throw new JOSEException(String.format("IV length of %d bits is required, got %d", IV_BIT_LENGTH, ByteUtils.safeBitLength(iv)));
 * @version 2017-06-01
		if (ByteUtils.safeBitLength(keyBytes) != keyLength) {
			throw new KeyLengthException("CEK key length mismatch: " + ByteUtils.safeBitLength(keyBytes) + " != " + keyLength);
 * @version 2017-06-01
		for (int i=1; i <= computeDigestCycles(ByteUtils.safeBitLength(md.getDigestLength()), keyLengthBits); i++) {
import com.nimbusds.jose.util.IntegerOverflowException;
 * @version 2017-06-01
		try {
			if (enc.cekBitLength() != ByteUtils.safeBitLength(cek.getEncoded())) {
				throw new KeyLengthException("The Content Encryption Key (CEK) length for " + enc + " must be " + enc.cekBitLength() + " bits");
			}
		} catch (IntegerOverflowException e) {
			throw new KeyLengthException("The Content Encryption Key (CEK) is too long: " + e.getMessage());
 * @version 2017-06-01
		if (enc.cekBitLength() != ByteUtils.safeBitLength(getKey().getEncoded())) {
 * @version 2017-06-01
			if (ByteUtils.safeBitLength(secretKeyBytes) != keyLength) {
import com.nimbusds.jose.util.*;
 * @version 2017-06-01
		try {
			return ByteUtils.safeBitLength(k.decode());
		} catch (IntegerOverflowException e) {
			throw new ArithmeticException(e.getMessage());
		}
import com.nimbusds.jose.util.*;
 * @version 2017-06-01
		try {
			return ByteUtils.safeBitLength(n.decode());
		} catch (IntegerOverflowException e) {
			throw new ArithmeticException(e.getMessage());
		}
 * @version 2017-06-01
	/**
	 * Returns the bit length of the specified byte length, preventing
	 * integer overflow.
	 *
	 * @param byteLength The byte length.
	 *
	 * @return The bit length.
	 *
	 * @throws IntegerOverflowException On a integer overflow.
	 */
	public static int safeBitLength(final int byteLength)
		throws IntegerOverflowException {
		
		long longResult = (long)byteLength * (long)8;
		if((long)((int)longResult) != longResult) {
			throw new IntegerOverflowException();
		} else {
			return (int)longResult;
		}
	}


	/**
	 * Returns the byte length of the specified byte array, preventing
	 * integer overflow.
	 *
	 * @param byteArray The byte array. May be {@code null}.
	 *
	 * @return The bite length, zero if the array is {@code null}.
	 *
	 * @throws IntegerOverflowException On a integer overflow.
	 */
	public static int safeBitLength(final byte[] byteArray)
		throws IntegerOverflowException {

		if (byteArray == null) {
			return 0;
		} else {
			return safeBitLength(byteArray.length);
		}
	}


/*
 * nimbus-jose-jwt
 *
 * Copyright 2012-2016, Connect2id Ltd and contributors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of the
 * License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed
 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */

package com.nimbusds.jose.util;


import com.nimbusds.jose.JOSEException;


/**
 * Integer overflow exception.
 */
public class IntegerOverflowException extends JOSEException {
	
	
	/**
	 * Creates a new integer overflow exception.
	 */
	public IntegerOverflowException() {
		super("Integer overflow");
	}
}
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jose.util.IntegerOverflowException;
import junit.framework.TestCase;
 * @version 2017-06-01
	public void testComputeLength()
		throws IntegerOverflowException {
import java.util.Arrays;
 * @author Quan Nguyen
 * @version 2017-06-01
	public void testAADLengthComputation()
		throws JOSEException {
	
	
	public void testIntegerOverflowHmacBypass()
		throws Exception {
		
		SecretKey inputKey = new SecretKeySpec(INPUT_KEY_256, "AES");
		
		Assert.assertArrayEquals("Input key", INPUT_KEY_256, inputKey.getEncoded());
		byte[] iv = new byte[16];
		byte[] aad = new byte[8];
		byte[] plaintext = new byte[536870928];
		for (int i = 0; i < plaintext.length; i++ ){
			// Doesn't matter what value is, but rand is too expensive for large array
			plaintext[i] = (byte) i;
		}
		
		AuthenticatedCipherText act;
		
		try {
			act = AESCBC.encryptAuthenticated(inputKey, iv, plaintext, aad, null, null);
		} catch (OutOfMemoryError e) {
			System.out.println("Test not run due to " + e);
			return;
		}
		
		byte[] ciphertext = act.getCipherText();
		byte[] authTag = act.getAuthenticationTag();
		
		// Now shift aad and ciphertext around so that HMAC doesn't change,
		// but the plaintext will change.
		int n = 0;
		byte[] buffer = new byte[aad.length + iv.length + ciphertext.length];
		System.arraycopy(aad, 0, buffer, n, aad.length);
		n += aad.length;
		System.arraycopy(iv, 0, buffer, n, iv.length);
		n += iv.length;
		System.arraycopy(ciphertext, 0, buffer, n, ciphertext.length);
		// Note that due to integer overflow :536870920 * 8 = 64
		int newAadSize = 536870920;
		byte[] newAad = Arrays.copyOfRange(buffer, 0, newAadSize);
		byte[] newIv = Arrays.copyOfRange(buffer, newAadSize, newAadSize + 16);
		byte[] newCiphertext = Arrays.copyOfRange(buffer, newAadSize + 16,
			buffer.length);
		
		try {
			byte[] decrypted = AESCBC.decryptAuthenticated(inputKey, newIv,
				newCiphertext, newAad,
				authTag, // Note that the authTag does NOT change.
				null, null);
			// Reaching this point means that the HMac check is
			// bypassed although the decrypted data is different
			// from plaintext.
			// Assert.assertArrayEquals(decrypted, plaintext);
			fail();
		} catch (JOSEException ignored) {
			// ok
		}
	}
	
import com.nimbusds.jose.JOSEException;
	
	
	public void testSafeBitLength_OK()
		throws JOSEException {
		
		assertEquals(8, ByteUtils.bitLength(1));
		assertEquals(16, ByteUtils.bitLength(2));
		assertEquals(32, ByteUtils.bitLength(4));
		assertEquals(64, ByteUtils.bitLength(8));
	}
	
	
	public void testSafeBitLength_IntegerOverflow() {
		
		try {
			ByteUtils.safeBitLength(Integer.MAX_VALUE);
			fail();
		} catch (IntegerOverflowException e) {
			assertEquals("Integer overflow", e.getMessage());
		}
	}
	
	
	public void testArraySafeBitLength_OK()
		throws JOSEException {
		
		assertEquals( 8, ByteUtils.bitLength(new byte[1]));
		assertEquals(16, ByteUtils.bitLength(new byte[2]));
		assertEquals(32, ByteUtils.bitLength(new byte[4]));
		assertEquals(64, ByteUtils.bitLength(new byte[8]));
	}
	
	
	public void testArraySafeBitLength_IntegerOverflow() {
		
		try {
			ByteUtils.safeBitLength(new byte[Integer.MAX_VALUE]);
			fail();
		} catch (OutOfMemoryError e) {
			System.out.println("Test not run due to " + e);
		} catch (IntegerOverflowException e) {
			assertEquals("Integer overflow", e.getMessage());
		}
	}