From da79b781af41ff815b812c49d9be434f5de52aa4 Mon Sep 17 00:00:00 2001 From: Legrandin Date: Mon, 13 May 2013 08:40:46 +0200 Subject: Add support for CMAC This patch adds support for CMAC (RFC4493, NIST SP800-38B). [dlitz@dlitz.net: Replaced MacMismatchError with ValueError] [dlitz@dlitz.net: Whitespace fixed with "git rebase --whitespace=fix"] --- lib/Crypto/Hash/CMAC.py | 277 ++++++++++++++++++++++++++++++++++ lib/Crypto/Hash/__init__.py | 3 +- lib/Crypto/SelfTest/Hash/__init__.py | 1 + lib/Crypto/SelfTest/Hash/test_CMAC.py | 249 ++++++++++++++++++++++++++++++ 4 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 lib/Crypto/Hash/CMAC.py create mode 100644 lib/Crypto/SelfTest/Hash/test_CMAC.py (limited to 'lib') diff --git a/lib/Crypto/Hash/CMAC.py b/lib/Crypto/Hash/CMAC.py new file mode 100644 index 0000000..1e3cf53 --- /dev/null +++ b/lib/Crypto/Hash/CMAC.py @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- +# +# Hash/CMAC.py - Implements the CMAC algorithm +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""CMAC (Cipher-based Message Authentication Code) algorithm + +CMAC is a MAC defined in `NIST SP 800-38B`_ and in RFC4493_ (for AES only) +and constructed using a block cipher. It was originally known as `OMAC1`_. + +The algorithm is sometimes named *X-CMAC* where *X* is the name +of the cipher (e.g. AES-CMAC). + +This is an example showing how to *create* an AES-CMAC: + + >>> from Crypto.Hash import CMAC + >>> from Crypto.Cipher import AES + >>> + >>> secret = b'Sixteen byte key' + >>> cobj = CMAC.new(secret, ciphermod=AES) + >>> cobj.update(b'Hello') + >>> print cobj.hexdigest() + +And this is an example showing how to *check* an AES-CMAC: + + >>> from Crypto.Hash import CMAC + >>> from Crypto.Cipher import AES + >>> + >>> # We have received a message 'msg' together + >>> # with its MAC 'mac' + >>> + >>> secret = b'Sixteen byte key' + >>> cobj = CMAC.new(secret, ciphermod=AES) + >>> cobj.update(msg) + >>> try: + >>> cobj.verify(mac) + >>> print "The message '%s' is authentic" % msg + >>> except ValueError: + >>> print "The message or the key is wrong" + +.. _`NIST SP 800-38B`: http://csrc.nist.gov/publications/nistpubs/800-38B/SP_800-38B.pdf +.. _RFC4493: http://www.ietf.org/rfc/rfc4493.txt +.. _OMAC1: http://www.nuee.nagoya-u.ac.jp/labs/tiwata/omac/omac.html +""" + +__all__ = ['new', 'digest_size', 'CMAC' ] + +import sys +if sys.version_info[0] == 2 and sys.version_info[1] == 1: + from Crypto.Util.py21compat import * +from Crypto.Util.py3compat import * + +from binascii import unhexlify + +from Crypto.Util.strxor import strxor +from Crypto.Util.number import long_to_bytes, bytes_to_long + +#: The size of the authentication tag produced by the MAC. +digest_size = None + +def _shift_bytes(bs, xor_lsb=0): + num = (bytes_to_long(bs)<<1) ^ xor_lsb + return long_to_bytes(num, len(bs))[-len(bs):] + +class CMAC(object): + """Class that implements CMAC""" + + #: The size of the authentication tag produced by the MAC. + digest_size = None + + def __init__(self, key, msg = None, ciphermod = None): + """Create a new CMAC object. + + :Parameters: + key : byte string + secret key for the CMAC object. + The key must be valid for the underlying cipher algorithm. + For instance, it must be 16 bytes long for AES-128. + msg : byte string + The very first chunk of the message to authenticate. + It is equivalent to an early call to `update`. Optional. + ciphermod : module + A cipher module from `Crypto.Cipher`. + The cipher's block size must be 64 or 128 bits. + It is recommended to use `Crypto.Cipher.AES`. + """ + + if ciphermod is None: + raise ValueError("ciphermod must be specified (try AES)") + + self._key = key + self._factory = ciphermod + + # Section 5.3 of NIST SP 800 38B + if ciphermod.block_size==8: + const_Rb = 0x1B + elif ciphermod.block_size==16: + const_Rb = 0x87 + else: + raise ValueError("For CMAC, block length of the selected cipher must be 8 or 16 bytes") + self.digest_size = ciphermod.block_size + + # MAC cache + self._tag = None + + # Compute sub-keys + cipher = ciphermod.new(key, ciphermod.MODE_ECB) + l = cipher.encrypt(bchr(0)*ciphermod.block_size) + if bord(l[0]) & 0x80: + self._k1 = _shift_bytes(l, const_Rb) + else: + self._k1 = _shift_bytes(l) + if bord(self._k1[0]) & 0x80: + self._k2 = _shift_bytes(self._k1, const_Rb) + else: + self._k2 = _shift_bytes(self._k1) + + # Initialize CBC cipher with zero IV + self._IV = bchr(0)*ciphermod.block_size + self._cipherCBC = ciphermod.new(key, ciphermod.MODE_CBC, self._IV) + + self._buffer = [] + self._buffer_len = 0 + + if msg is not None: + self.update(msg) + + def update(self, msg): + """Continue authentication of a message by consuming the next chunk of data. + + Repeated calls are equivalent to a single call with the concatenation + of all the arguments. In other words: + + >>> m.update(a); m.update(b) + + is equivalent to: + + >>> m.update(a+b) + + :Parameters: + msg : byte string + The next chunk of the message being authenticated + """ + + self._buffer += [ msg ] + self._buffer_len += len(msg) + + # MAC data as you go but leave at least 1 byte in the buffer + bsize = self._cipherCBC.block_size + if self._buffer_len>bsize: + data = b("").join(self._buffer) + self._buffer_len = self._buffer_len&(bsize-1) + if self._buffer_len==0: + self._buffer_len=bsize + self._buffer = [ data[-self._buffer_len:] ] + self._IV = self._cipherCBC.encrypt(data[:-self._buffer_len])[-bsize:] + + def copy(self): + """Return a copy ("clone") of the MAC object. + + The copy will have the same internal state as the original MAC + object. + This can be used to efficiently compute the MAC of strings that + share a common initial substring. + + :Returns: A `CMAC` object + """ + obj = CMAC(self._key, ciphermod=self._factory) + + # Deep copy + for m in [ '_tag', '_buffer', '_buffer_len', '_k1', '_k2', '_IV']: + setattr(obj, m, getattr(self, m)) + obj._cipherCBC = self._factory.new(self._key, self._factory.MODE_CBC, self._IV) + return obj + + def digest(self): + """Return the **binary** (non-printable) MAC of the message that has + been authenticated so far. + + This method does not change the state of the MAC object. + You can continue updating the object after calling this function. + + :Return: A byte string of `digest_size` bytes. It may contain non-ASCII + characters, including null bytes. + """ + + if not self._tag: + data = b("").join(self._buffer) + bsize = self._cipherCBC.block_size + if len(data)==bsize: + last_block = strxor(data, self._k1) + else: + last_block = strxor(data+bchr(128)+bchr(0)*(bsize-1-len(data)), self._k2) + self._tag = self._cipherCBC.encrypt(last_block) + + return self._tag + + def hexdigest(self): + """Return the **printable** MAC of the message that has been + authenticated so far. + + This method does not change the state of the MAC object. + + :Return: A string of 2* `digest_size` bytes. It contains only + hexadecimal ASCII digits. + """ + return "".join(["%02x" % bord(x) + for x in tuple(self.digest())]) + + def verify(self, mac_tag): + """Verify that a given **binary** MAC (computed by another party) is valid. + + :Parameters: + mac_tag : byte string + The expected MAC of the message. + :Raises ValueError: + if the MAC does not match. It means that the message + has been tampered with or that the MAC key is incorrect. + """ + + mac = self.digest() + res = 0 + # Constant-time comparison + for x,y in zip(mac, mac_tag): + res |= bord(x) ^ bord(y) + if res or len(mac_tag)!=self.digest_size: + raise ValueError("MAC check failed") + + def hexverify(self, hex_mac_tag): + """Verify that a given **printable** MAC (computed by another party) is valid. + + :Parameters: + hex_mac_tag : string + The expected MAC of the message, as a hexadecimal string. + :Raises ValueError: + if the MAC does not match. It means that the message + has been tampered with or that the MAC key is incorrect. + """ + + self.verify(unhexlify(hex_mac_tag)) + +def new(key, msg = None, ciphermod = None): + """Create a new CMAC object. + + :Parameters: + key : byte string + secret key for the CMAC object. + The key must be valid for the underlying cipher algorithm. + For instance, it must be 16 bytes long for AES-128. + msg : byte string + The very first chunk of the message to authenticate. + It is equivalent to an early call to `CMAC.update`. Optional. + ciphermod : module + A cipher module from `Crypto.Cipher`. + The cipher's block size must be 64 or 128 bits. + Default is `Crypto.Cipher.AES`. + + :Returns: A `CMAC` object + """ + return CMAC(key, msg, ciphermod) diff --git a/lib/Crypto/Hash/__init__.py b/lib/Crypto/Hash/__init__.py index 1050c78..f16e253 100644 --- a/lib/Crypto/Hash/__init__.py +++ b/lib/Crypto/Hash/__init__.py @@ -50,7 +50,8 @@ The hashing modules here all support the interface described in `PEP """ __all__ = ['HMAC', 'MD2', 'MD4', 'MD5', 'RIPEMD160', 'SHA1', - 'SHA224', 'SHA256', 'SHA384', 'SHA512'] + 'SHA224', 'SHA256', 'SHA384', 'SHA512', 'CMAC'] + __revision__ = "$Id$" import sys diff --git a/lib/Crypto/SelfTest/Hash/__init__.py b/lib/Crypto/SelfTest/Hash/__init__.py index d6c8e57..e2d9cd8 100644 --- a/lib/Crypto/SelfTest/Hash/__init__.py +++ b/lib/Crypto/SelfTest/Hash/__init__.py @@ -29,6 +29,7 @@ __revision__ = "$Id$" def get_tests(config={}): tests = [] from Crypto.SelfTest.Hash import test_HMAC; tests += test_HMAC.get_tests(config=config) + from Crypto.SelfTest.Hash import test_CMAC; tests += test_CMAC.get_tests(config=config) from Crypto.SelfTest.Hash import test_MD2; tests += test_MD2.get_tests(config=config) from Crypto.SelfTest.Hash import test_MD4; tests += test_MD4.get_tests(config=config) from Crypto.SelfTest.Hash import test_MD5; tests += test_MD5.get_tests(config=config) diff --git a/lib/Crypto/SelfTest/Hash/test_CMAC.py b/lib/Crypto/SelfTest/Hash/test_CMAC.py new file mode 100644 index 0000000..f16d89f --- /dev/null +++ b/lib/Crypto/SelfTest/Hash/test_CMAC.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +# +# SelfTest/Hash/CMAC.py: Self-test for the CMAC module +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""Self-test suite for Crypto.Hash.CMAC""" + +import sys +if sys.version_info[0] == 2 and sys.version_info[1] == 1: + from Crypto.Util.py21compat import * +from Crypto.Util.py3compat import * + +from common import dict + +from Crypto.Hash import CMAC +from Crypto.Cipher import AES, DES3 + +# This is a list of (key, data, result, description, module) tuples. +test_data = [ + + ## Test vectors from RFC 4493 ## + ## The are also in NIST SP 800 38B D.2 ## + ( '2b7e151628aed2a6abf7158809cf4f3c', + '', + 'bb1d6929e95937287fa37d129b756746', + 'RFC 4493 #1', + AES + ), + + ( '2b7e151628aed2a6abf7158809cf4f3c', + '6bc1bee22e409f96e93d7e117393172a', + '070a16b46b4d4144f79bdd9dd04a287c', + 'RFC 4493 #2', + AES + ), + + ( '2b7e151628aed2a6abf7158809cf4f3c', + '6bc1bee22e409f96e93d7e117393172a'+ + 'ae2d8a571e03ac9c9eb76fac45af8e51'+ + '30c81c46a35ce411', + 'dfa66747de9ae63030ca32611497c827', + 'RFC 4493 #3', + AES + ), + + ( '2b7e151628aed2a6abf7158809cf4f3c', + '6bc1bee22e409f96e93d7e117393172a'+ + 'ae2d8a571e03ac9c9eb76fac45af8e51'+ + '30c81c46a35ce411e5fbc1191a0a52ef'+ + 'f69f2445df4f9b17ad2b417be66c3710', + '51f0bebf7e3b9d92fc49741779363cfe', + 'RFC 4493 #4', + AES + ), + + ## The rest of Appendix D of NIST SP 800 38B + ## was not totally correct. + ## Values in Examples 14, 15, 18, and 19 were wrong. + ## The updated test values are published in: + ## http://csrc.nist.gov/publications/nistpubs/800-38B/Updated_CMAC_Examples.pdf + + ( '8e73b0f7da0e6452c810f32b809079e5'+ + '62f8ead2522c6b7b', + '', + 'd17ddf46adaacde531cac483de7a9367', + 'NIST SP 800 38B D.2 Example 5', + AES + ), + + ( '8e73b0f7da0e6452c810f32b809079e5'+ + '62f8ead2522c6b7b', + '6bc1bee22e409f96e93d7e117393172a', + '9e99a7bf31e710900662f65e617c5184', + 'NIST SP 800 38B D.2 Example 6', + AES + ), + + ( '8e73b0f7da0e6452c810f32b809079e5'+ + '62f8ead2522c6b7b', + '6bc1bee22e409f96e93d7e117393172a'+ + 'ae2d8a571e03ac9c9eb76fac45af8e51'+ + '30c81c46a35ce411', + '8a1de5be2eb31aad089a82e6ee908b0e', + 'NIST SP 800 38B D.2 Example 7', + AES + ), + + ( '8e73b0f7da0e6452c810f32b809079e5'+ + '62f8ead2522c6b7b', + '6bc1bee22e409f96e93d7e117393172a'+ + 'ae2d8a571e03ac9c9eb76fac45af8e51'+ + '30c81c46a35ce411e5fbc1191a0a52ef'+ + 'f69f2445df4f9b17ad2b417be66c3710', + 'a1d5df0eed790f794d77589659f39a11', + 'NIST SP 800 38B D.2 Example 8', + AES + ), + + ( '603deb1015ca71be2b73aef0857d7781'+ + '1f352c073b6108d72d9810a30914dff4', + '', + '028962f61b7bf89efc6b551f4667d983', + 'NIST SP 800 38B D.3 Example 9', + AES + ), + + ( '603deb1015ca71be2b73aef0857d7781'+ + '1f352c073b6108d72d9810a30914dff4', + '6bc1bee22e409f96e93d7e117393172a', + '28a7023f452e8f82bd4bf28d8c37c35c', + 'NIST SP 800 38B D.3 Example 10', + AES + ), + + ( '603deb1015ca71be2b73aef0857d7781'+ + '1f352c073b6108d72d9810a30914dff4', + '6bc1bee22e409f96e93d7e117393172a'+ + 'ae2d8a571e03ac9c9eb76fac45af8e51'+ + '30c81c46a35ce411', + 'aaf3d8f1de5640c232f5b169b9c911e6', + 'NIST SP 800 38B D.3 Example 11', + AES + ), + + ( '603deb1015ca71be2b73aef0857d7781'+ + '1f352c073b6108d72d9810a30914dff4', + '6bc1bee22e409f96e93d7e117393172a'+ + 'ae2d8a571e03ac9c9eb76fac45af8e51'+ + '30c81c46a35ce411e5fbc1191a0a52ef'+ + 'f69f2445df4f9b17ad2b417be66c3710', + 'e1992190549f6ed5696a2c056c315410', + 'NIST SP 800 38B D.3 Example 12', + AES + ), + + ( '8aa83bf8cbda1062'+ + '0bc1bf19fbb6cd58'+ + 'bc313d4a371ca8b5', + '', + 'b7a688e122ffaf95', + 'NIST SP 800 38B D.4 Example 13', + DES3 + ), + + ( '8aa83bf8cbda1062'+ + '0bc1bf19fbb6cd58'+ + 'bc313d4a371ca8b5', + '6bc1bee22e409f96', + '8e8f293136283797', + 'NIST SP 800 38B D.4 Example 14', + DES3 + ), + + ( '8aa83bf8cbda1062'+ + '0bc1bf19fbb6cd58'+ + 'bc313d4a371ca8b5', + '6bc1bee22e409f96'+ + 'e93d7e117393172a'+ + 'ae2d8a57', + '743ddbe0ce2dc2ed', + 'NIST SP 800 38B D.4 Example 15', + DES3 + ), + + ( '8aa83bf8cbda1062'+ + '0bc1bf19fbb6cd58'+ + 'bc313d4a371ca8b5', + '6bc1bee22e409f96'+ + 'e93d7e117393172a'+ + 'ae2d8a571e03ac9c'+ + '9eb76fac45af8e51', + '33e6b1092400eae5', + 'NIST SP 800 38B D.4 Example 16', + DES3 + ), + + ( '4cf15134a2850dd5'+ + '8a3d10ba80570d38', + '', + 'bd2ebf9a3ba00361', + 'NIST SP 800 38B D.7 Example 17', + DES3 + ), + + ( '4cf15134a2850dd5'+ + '8a3d10ba80570d38', + '6bc1bee22e409f96', + '4ff2ab813c53ce83', + 'NIST SP 800 38B D.7 Example 18', + DES3 + ), + + ( '4cf15134a2850dd5'+ + '8a3d10ba80570d38', + '6bc1bee22e409f96'+ + 'e93d7e117393172a'+ + 'ae2d8a57', + '62dd1b471902bd4e', + 'NIST SP 800 38B D.7 Example 19', + DES3 + ), + + ( '4cf15134a2850dd5'+ + '8a3d10ba80570d38', + '6bc1bee22e409f96'+ + 'e93d7e117393172a'+ + 'ae2d8a571e03ac9c'+ + '9eb76fac45af8e51', + '31b1e431dabc4eb8', + 'NIST SP 800 38B D.7 Example 20', + DES3 + ), + +] + +def get_tests(config={}): + global test_data + from common import make_mac_tests + + # Add new() parameters to the back of each test vector + params_test_data = [] + for row in test_data: + t = list(row) + t[4] = dict(ciphermod=t[4]) + params_test_data.append(t) + + return make_mac_tests(CMAC, "CMAC", params_test_data) + +if __name__ == '__main__': + import unittest + suite = lambda: unittest.TestSuite(get_tests()) + unittest.main(defaultTest='suite') -- cgit v1.2.1