ruby / openssl

Provides SSL, TLS and general purpose cryptography.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

AES-128-CCM - "iv must be 7 bytes"

harry-m opened this issue · comments

Issue: when trying to decrypt AES-128-CCM, I pass a 16 byte IV but receive an error saying that a 7byte IV is required:

code.rb:27:in `iv=': iv must be 7 bytes (ArgumentError)

The code:

def decrypt(ct_blob, secret)
  key = OpenSSL::PKCS5.pbkdf2_hmac(Base64.decode64(secret), Base64.decode64(ct_blob[:salt]), 1000, 16, 'SHA256')
  c = OpenSSL::Cipher::AES128.new(:CCM)

  c.decrypt    
  c.key = key
  c.iv = Base64.decode64(ct_blob[:iv])

  c.update(Base64.decode64(ct_blob[:ct]))
end

I'm very confused by this because my understanding was that AES uses a 16 byte block and that the IV is the same as the block size. The size of Base64.decode64(ct_blob[:iv]) is a 16 bytes as expected.

Am I missing something obvious? I have traced it back to a check in ossl_cipher.c:515:

if (RSTRING_LEN(iv) != iv_len)
	ossl_raise(rb_eArgError, "iv must be %d bytes", iv_len);

But haven't got very far poking around in openssl - I wanted to check because it seems much more likely that I'm doing something wrong than that there's a bug in openssl.

Grateful for any steer.

CCM mode is based on CTR mode. The nonce (which OpenSSL conveniently calls "IV") will be shorter than the block length. According to RFC 3610, a valid nonce length for AES-CCM is 7..13 octets and OpenSSL happens to choose 7 octets as the default.

Due to the streaming-style API of OpenSSL::Cipher (EVP_CIPHER_CTX), the CCM support unfortunately is convoluted. You will have to use OpenSSL::Cipher#auth_tag_len=, #iv_len=, and #ccm_data_len= in the correct order (this is different from other AEAD modes such as GCM, which don't require these).

You might find this test case in Ruby/OpenSSL useful:

def test_aes_ccm
# RFC 3610 Section 8, Test Case 1
key = ["c0c1c2c3c4c5c6c7c8c9cacbcccdcecf"].pack("H*")
iv = ["00000003020100a0a1a2a3a4a5"].pack("H*")
aad = ["0001020304050607"].pack("H*")
pt = ["08090a0b0c0d0e0f101112131415161718191a1b1c1d1e"].pack("H*")
ct = ["588c979a61c663d2f066d0c2c0f989806d5f6b61dac384"].pack("H*")
tag = ["17e8d12cfdf926e0"].pack("H*")
kwargs = {auth_tag_len: 8, iv_len: 13, key: key, iv: iv}
cipher = new_encryptor("aes-128-ccm", **kwargs, ccm_data_len: pt.length, auth_data: aad)
assert_equal ct, cipher.update(pt) << cipher.final
assert_equal tag, cipher.auth_tag
cipher = new_decryptor("aes-128-ccm", **kwargs, ccm_data_len: ct.length, auth_tag: tag, auth_data: aad)
assert_equal pt, cipher.update(ct) << cipher.final
# truncated tag is accepted
cipher = new_encryptor("aes-128-ccm", **kwargs, ccm_data_len: pt.length, auth_data: aad)
assert_equal ct, cipher.update(pt) << cipher.final
assert_equal tag[0, 8], cipher.auth_tag(8)
cipher = new_decryptor("aes-128-ccm", **kwargs, ccm_data_len: ct.length, auth_tag: tag[0, 8], auth_data: aad)
assert_equal pt, cipher.update(ct) << cipher.final
# wrong tag is rejected
tag2 = tag.dup
tag2.setbyte(-1, (tag2.getbyte(-1) + 1) & 0xff)
cipher = new_decryptor("aes-128-ccm", **kwargs, ccm_data_len: ct.length, auth_tag: tag2, auth_data: aad)
assert_raise(OpenSSL::Cipher::CipherError) { cipher.update(ct) }
# wrong aad is rejected
aad2 = aad[0..-2] << aad[-1].succ
cipher = new_decryptor("aes-128-ccm", **kwargs, ccm_data_len: ct.length, auth_tag: tag, auth_data: aad2)
assert_raise(OpenSSL::Cipher::CipherError) { cipher.update(ct) }
# wrong ciphertext is rejected
ct2 = ct[0..-2] << ct[-1].succ
cipher = new_decryptor("aes-128-ccm", **kwargs, ccm_data_len: ct2.length, auth_tag: tag, auth_data: aad)
assert_raise(OpenSSL::Cipher::CipherError) { cipher.update(ct2) }
end if has_cipher?("aes-128-ccm") &&
OpenSSL::Cipher.new("aes-128-ccm").authenticated? &&
OpenSSL::OPENSSL_VERSION_NUMBER >= 0x1010103f # version >= 1.1.1c