Skip to content

Conversation

zeroSteiner
Copy link
Contributor

As it is currently written the Cipher object can not be used in CCM mode with additional authenticate data. This is because per the OpenSSL documentation, when a Cipher is used in CCM mode the ciphertext, plaintext length must be specified to EVP_CipherUpdate as the inl parameter while both in and out are set to NULL (see the "CCM Mode" section of the OpenSSL docs here: https://www.openssl.org/docs/man1.1.1/man3/EVP_CipherUpdate.html). This PR changes the implementation of #update to allow the data parameter to be specified as an integer which is assumed to be the length of the ciphertext / plaintext for the update operation effectively fixing the limitation and allowing users to utilize the CCM mode.

Before the patch there would be an exception when trying to set #auth_data stating that additional data could not be set. After the patch, users can set the length of the message to encrypt using #update and fully utilize this cipher mode. Note that this behavior is only necessary for CCM mode. GCM on the other hand works as is, and does not require specifying the length of the plaintext / ciphertext.

Example Usage

Example functioning CCM mode implementation (requires changes proposed by this PR):

require 'openssl'

auth_data = 'ABCDABCD'
message = 'WXYZWXYZ'

def bin_to_hex(s)
  s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
end

def build_cipher(mode, data)
  cipher = OpenSSL::Cipher.new('AES-128-CCM')
  cipher.send(mode)
  cipher.auth_tag_len = 16
  cipher.iv_len = 11
  cipher.iv = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  cipher.key = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  cipher.update(data.length)  # set the plaintext / ciphertext length
  cipher
end

puts "Gem Version:     #{OpenSSL::VERSION}"
puts "OpenSSL Version: #{OpenSSL::OPENSSL_VERSION}"

cipher = build_cipher(:encrypt, message)
cipher.auth_data = auth_data

encrypted = cipher.update(message) + cipher.final
auth_tag = cipher.auth_tag

puts "encrypted: #{bin_to_hex(encrypted)}"
puts "signature: #{bin_to_hex(auth_tag)}"

cipher = build_cipher(:decrypt, encrypted)
cipher.auth_data = auth_data
cipher.auth_tag = auth_tag
decrypted = cipher.update(encrypted) + cipher.final

puts "decrypted: #{decrypted}"

Output:

Gem Version:     2.2.0
OpenSSL Version: OpenSSL 1.1.1d FIPS  10 Sep 2019
encrypted: 79c293318d0ca535
signature: 9dddaf882f7c94f333895c9e69a63d40
decrypted: WXYZWXYZ

If for whatever reason this implementation isn't ideal, the gist of an alternative is that the length of the plaintext / ciphertext needs to be able to be specified in a call similar to EVP_CipherUpdate(ctx, NULL, &len, NULL, text_len) before the AAD is set. An example implementation in C is available on the OpenSSL wiki here: https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption#Authenticated_Encryption_using_CCM_mode.

@rhenium
Copy link
Member

rhenium commented Mar 31, 2020

It should be a separate method rather than an overload of #update. I have no idea why OpenSSL did not use the EVP_CIPHER_CTX_ctrl() interface to notify the total length, but we don't need to follow that.

Perhaps #ccm_message_len=, as CCM is the only mode that requires this? I don't know if "message" is the appropriate word here.

This needs a test case. RFC 3610 includes some test vectors for AES-CCM.

@zeroSteiner zeroSteiner changed the title Allow specifying the text length to #update Allow specifying the data length for CCM mode Mar 31, 2020
@zeroSteiner
Copy link
Contributor Author

I reverted the changes that overloaded the #update method and replaced them with a new #ccm_data_len= method. I opted to utilize "data" since you didn't seem tied to "message" and there are more references to "data" than "message" in the existing API.

I also added unit tests using Test Vector 1 from Section 8 of RFC 3610. It looks like the unit tests are failing on a number of systems where the aes-128-ccm cipher is not supported.

@rhenium
Copy link
Member

rhenium commented Mar 31, 2020

It looks like the unit tests are failing on a number of systems where the aes-128-ccm cipher is not supported.

The test case can be defined conditionally. The AES-OCB test case does this.

Please rebase commits and it's good to merge 👍

@zeroSteiner zeroSteiner force-pushed the fix/aead/ccm-mode-in-len branch from 16448fc to 6e110c3 Compare March 31, 2020 20:03
@zeroSteiner
Copy link
Contributor Author

Changes made and commits squashed. Thanks for the review.

@zeroSteiner zeroSteiner force-pushed the fix/aead/ccm-mode-in-len branch 2 times, most recently from 2f85c6a to db06bd3 Compare April 16, 2020 22:12
@zeroSteiner
Copy link
Contributor Author

I fixed an instance where the AES-128-CCM cipher was not being identified as an AEAD cipher by checking #authenticated? as well, but I'm still getting an error with openssl-1.1.0l for some reason.

@zeroSteiner zeroSteiner force-pushed the fix/aead/ccm-mode-in-len branch from b34c3d5 to 542cf13 Compare April 17, 2020 13:33
@rhenium
Copy link
Member

rhenium commented Apr 20, 2020

I'm sorry for the long silence.

I'm suspect the failure with 1.1.0l is related to OpenSSL commit openssl/openssl@67c81ec, which went into OpenSSL 1.1.1 but was not backported to 1.1.0. Reordering the keyword arguments to new_decryptor so that ccm_data_len will come after tag_len (to match the example in https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption#Authenticated_Encryption_using_CCM_mode) might fix it.

@zeroSteiner
Copy link
Contributor Author

I tried another test configuration to replicate the ordering from the wiki you referenced as closely as I could with no luck. I'll have to look into this a little more.

@rhenium
Copy link
Member

rhenium commented Apr 21, 2020

This appears to be another OpenSSL bug, which was fixed by openssl/openssl@197421b / OpenSSL 1.1.1.

The 'final' call is a fundamental part of the EVP_Cipher* API and my view now is that OpenSSL 1.1.0 didn't really support CCM mode and nobody tried to use it.

I think the test code can just be skipped for OpenSSL < 1.1.1.

Allow specifying just length to #update

CCM mode ciphers need to specify the total plaintext or ciphertext
length to EVP_CipherUpdate.

Update the link to the tests file

Define Cipher#ccm_data_len= for CCM mode ciphers

Add a unit test for CCM mode

Also check CCM is authenticated when testing
@zeroSteiner zeroSteiner force-pushed the fix/aead/ccm-mode-in-len branch from 74c6500 to bb38169 Compare April 22, 2020 00:07
@zeroSteiner
Copy link
Contributor Author

Alright all unit tests are passing and it's been rebased again into one commit. Thanks alot for your help in working through the test issues.

@rhenium rhenium merged commit c14e2b2 into ruby:master Apr 22, 2020
@rhenium
Copy link
Member

rhenium commented Apr 22, 2020

Looks good to me. Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants