diff --git a/scripts/build_ffi.py b/scripts/build_ffi.py index 4260f88..51e077f 100644 --- a/scripts/build_ffi.py +++ b/scripts/build_ffi.py @@ -205,6 +205,7 @@ def make_flags(prefix, fips): flags.append("--enable-aesgcm-stream") flags.append("--enable-aesgcm") + flags.append("--enable-aessiv") # hashes and MACs flags.append("--enable-sha") @@ -358,6 +359,7 @@ def get_features(local_wolfssl, features): features["SHA3"] = 1 if '#define WOLFSSL_SHA3' in defines else 0 features["DES3"] = 0 if '#define NO_DES3' in defines else 1 features["AES"] = 0 if '#define NO_AES' in defines else 1 + features["AES_SIV"] = 1 if '#define WOLFSSL_AES_SIV' in defines else 0 features["CHACHA"] = 1 if '#define HAVE_CHACHA' in defines else 0 features["HMAC"] = 0 if '#define NO_HMAC' in defines else 1 features["RSA"] = 0 if '#define NO_RSA' in defines else 1 @@ -472,6 +474,7 @@ def build_ffi(local_wolfssl, features): int SHA3_ENABLED = """ + str(features["SHA3"]) + """; int DES3_ENABLED = """ + str(features["DES3"]) + """; int AES_ENABLED = """ + str(features["AES"]) + """; + int AES_SIV_ENABLED = """ + str(features["AES_SIV"]) + """; int CHACHA_ENABLED = """ + str(features["CHACHA"]) + """; int HMAC_ENABLED = """ + str(features["HMAC"]) + """; int RSA_ENABLED = """ + str(features["RSA"]) + """; @@ -509,6 +512,7 @@ def build_ffi(local_wolfssl, features): extern int SHA3_ENABLED; extern int DES3_ENABLED; extern int AES_ENABLED; + extern int AES_SIV_ENABLED; extern int CHACHA_ENABLED; extern int HMAC_ENABLED; extern int RSA_ENABLED; @@ -645,6 +649,25 @@ def build_ffi(local_wolfssl, features): word32 authTagSz); """ + if features["AES"] and features["AES_SIV"]: + cdef += """ + typedef struct AesSivAssoc_s { + const byte* assoc; + word32 assocSz; + } AesSivAssoc; + int wc_AesSivEncrypt(const byte* key, word32 keySz, const byte* assoc, + word32 assocSz, const byte* nonce, word32 nonceSz, + const byte* in, word32 inSz, byte* siv, byte* out); + int wc_AesSivDecrypt(const byte* key, word32 keySz, const byte* assoc, + word32 assocSz, const byte* nonce, word32 nonceSz, + const byte* in, word32 inSz, byte* siv, byte* out); + int wc_AesSivEncrypt_ex(const byte* key, word32 keySz, const AesSivAssoc* assoc, + word32 numAssoc, const byte* nonce, word32 nonceSz, + const byte* in, word32 inSz, byte* siv, byte* out); + int wc_AesSivDecrypt_ex(const byte* key, word32 keySz, const AesSivAssoc* assoc, + word32 numAssoc, const byte* nonce, word32 nonceSz, + const byte* in, word32 inSz, byte* siv, byte* out); + """ if features["CHACHA"]: cdef += """ @@ -1000,6 +1023,7 @@ def main(ffibuilder): "SHA3": 1, "DES3": 1, "AES": 1, + "AES_SIV": 1, "HMAC": 1, "RSA": 1, "RSA_BLINDING": 1, diff --git a/tests/test_ciphers.py b/tests/test_ciphers.py index e0b0567..ad401a8 100644 --- a/tests/test_ciphers.py +++ b/tests/test_ciphers.py @@ -21,6 +21,7 @@ # pylint: disable=redefined-outer-name from collections import namedtuple +import random import pytest from wolfcrypt._ffi import ffi as _ffi from wolfcrypt._ffi import lib as _lib @@ -35,6 +36,9 @@ if _lib.AES_ENABLED: from wolfcrypt.ciphers import Aes +if _lib.AES_SIV_ENABLED: + from wolfcrypt.ciphers import AesSiv + if _lib.CHACHA_ENABLED: from wolfcrypt.ciphers import ChaCha @@ -57,7 +61,7 @@ @pytest.fixture def vectors(): - TestVector = namedtuple("TestVector", """key iv plaintext ciphertext + TestVector = namedtuple("TestVector", """key iv plaintext ciphertext ciphertext_ctr raw_key pkcs8_key pem""") TestVector.__new__.__defaults__ = (None,) * len(TestVector._fields) @@ -727,3 +731,142 @@ def test_ed448_sign_verify(ed448_private, ed448_public): # private object holds both private and public info, so it can also verify # using the known public key. assert ed448_private.verify(signature, plaintext) + + +@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled") +def test_aessiv_encrypt_decrypt(): + key = random.randbytes(32) + aessiv = AesSiv(key) + associated_data = random.randbytes(16) + nonce = random.randbytes(12) + plaintext = random.randbytes(16) + siv, ciphertext = aessiv.encrypt(associated_data, nonce, plaintext) + assert aessiv.decrypt(associated_data, nonce, siv, ciphertext) == plaintext + + +# +# Test vectors copied from RFC-5297. +# +TEST_VECTOR_KEY_RFC5297 = bytes.fromhex( + "7f7e7d7c 7b7a7978 77767574 73727170" + "40414243 44454647 48494a4b 4c4d4e4f" +) +TEST_VECTOR_ASSOCIATED_DATA_1_RFC5297 = bytes.fromhex( + "00112233 44556677 8899aabb ccddeeff" + "deaddada deaddada ffeeddcc bbaa9988" + "77665544 33221100" +) +TEST_VECTOR_ASSOCIATED_DATA_2_RFC5297 = bytes.fromhex( + "10203040 50607080 90a0" +) +TEST_VECTOR_NONCE_RFC5297 = bytes.fromhex( + "09f91102 9d74e35b d84156c5 635688c0" +) +TEST_VECTOR_PLAINTEXT_RFC5297 = bytes.fromhex( + "74686973 20697320 736f6d65 20706c61" + "696e7465 78742074 6f20656e 63727970" + "74207573 696e6720 5349562d 414553" +) +TEST_VECTOR_SIV_RFC5297 = bytes.fromhex( + "7bdb6e3b 432667eb 06f4d14b ff2fbd0f" +) +TEST_VECTOR_CIPHERTEXT_RFC5297 = bytes.fromhex( + "cb900f2f ddbe4043 26601965 c889bf17" + "dba77ceb 094fa663 b7a3f748 ba8af829" + "ea64ad54 4a272e9c 485b62a3 fd5c0d" +) + + +@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled") +def test_aessiv_encrypt_kat_rfc5297(): + """ + Known-answer test using test vectors from RFC-5297. + """ + aessiv = AesSiv(TEST_VECTOR_KEY_RFC5297) + associated_data = [ + TEST_VECTOR_ASSOCIATED_DATA_1_RFC5297, + TEST_VECTOR_ASSOCIATED_DATA_2_RFC5297, + ] + siv, ciphertext = aessiv.encrypt( + associated_data, + TEST_VECTOR_NONCE_RFC5297, + TEST_VECTOR_PLAINTEXT_RFC5297 + ) + assert siv == TEST_VECTOR_SIV_RFC5297 + assert ciphertext == TEST_VECTOR_CIPHERTEXT_RFC5297 + + +@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled") +def test_aessiv_decrypt_kat_rfc5297(): + """ + Known-answer test using test vectors from RFC-5297. + """ + aessiv = AesSiv(TEST_VECTOR_KEY_RFC5297) + associated_data = ( + TEST_VECTOR_ASSOCIATED_DATA_1_RFC5297, + TEST_VECTOR_ASSOCIATED_DATA_2_RFC5297, + ) + plaintext = aessiv.decrypt( + associated_data, + TEST_VECTOR_NONCE_RFC5297, + TEST_VECTOR_SIV_RFC5297, + TEST_VECTOR_CIPHERTEXT_RFC5297 + ) + assert plaintext == TEST_VECTOR_PLAINTEXT_RFC5297 + + +# +# Test vectors copied from OpenSSL library file evpciph_aes_siv.txt.. +# +TEST_VECTOR_KEY_OPENSSL = bytes.fromhex( + "fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" +) +TEST_VECTOR_ASSOCIATED_DATA_OPENSSL = bytes.fromhex( + "101112131415161718191a1b1c1d1e1f2021222324252627" +) +TEST_VECTOR_NONCE_OPENSSL = b"" +TEST_VECTOR_PLAINTEXT_OPENSSL = bytes.fromhex( + "112233445566778899aabbccddee" +) +TEST_VECTOR_SIV_OPENSSL = bytes.fromhex( + "85632d07c6e8f37f950acd320a2ecc93" +) +TEST_VECTOR_CIPHERTEXT_OPENSSL = bytes.fromhex( + "40c02b9690c4dc04daef7f6afe5c" +) + + +@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled") +def test_aessiv_encrypt_kat_openssl(): + """ + Known-answer test using test vectors from OpenSSL. + + This also tests calling AesSiv with a single associated data block, not + provided as a list of blocks. + """ + aessiv = AesSiv(TEST_VECTOR_KEY_OPENSSL) + siv, ciphertext = aessiv.encrypt( + TEST_VECTOR_ASSOCIATED_DATA_OPENSSL, + TEST_VECTOR_NONCE_OPENSSL, + TEST_VECTOR_PLAINTEXT_OPENSSL + ) + assert siv == TEST_VECTOR_SIV_OPENSSL + assert ciphertext == TEST_VECTOR_CIPHERTEXT_OPENSSL + + +@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled") +def test_aessiv_decrypt_kat_openssl(): + """ + Known-answer test using test vectors from OpenSSL. + + This also tests calling AesSiv with a single associated data block, not + provided as a list of blocks. + """ + aessiv = AesSiv(TEST_VECTOR_KEY_OPENSSL) + plaintext = aessiv.decrypt( + TEST_VECTOR_ASSOCIATED_DATA_OPENSSL, + TEST_VECTOR_NONCE_OPENSSL, + TEST_VECTOR_SIV_OPENSSL, + TEST_VECTOR_CIPHERTEXT_OPENSSL + ) + assert plaintext == TEST_VECTOR_PLAINTEXT_OPENSSL diff --git a/wolfcrypt/ciphers.py b/wolfcrypt/ciphers.py index 1df3177..ef3737b 100644 --- a/wolfcrypt/ciphers.py +++ b/wolfcrypt/ciphers.py @@ -275,6 +275,113 @@ def _decrypt(self, destination, source): else: raise ValueError("Invalid mode associated to cipher") +if _lib.AES_SIV_ENABLED: + class AesSiv(object): + """ + AES-SIV (Synthetic Initialization Vector) implementation as described in RFC 5297. + """ + _key_sizes = [16, 24, 32] + block_size = 16 + + def __init__(self, key): + self._key = t2b(key) + if len(self._key) not in AesSiv._key_sizes: + raise ValueError("key must be %s in length, not %d" % + (AesSiv._key_sizes, len(self._key))) + + def encrypt(self, associated_data, nonce, plaintext): + """ + Encrypt plaintext data using the nonce provided. The associated + data is not encrypted but is included in the authentication tag. + + Associated data may be provided as single str or bytes, or as a + list of str or bytes in case of multiple blocks. + + Returns a tuple of the IV and ciphertext. + """ + # Prepare the associated data blocks. Make sure to hold on to the + # returned references until the C function has been called in order + # to prevent garbage collection of them until the function is done. + associated_data, _refs = ( + AesSiv._prepare_associated_data(associated_data)) + nonce = t2b(nonce) + plaintext = t2b(plaintext) + siv = _ffi.new("byte[%d]" % AesSiv.block_size) + ciphertext = _ffi.new("byte[%d]" % len(plaintext)) + ret = _lib.wc_AesSivEncrypt_ex(self._key, len(self._key), + associated_data, len(associated_data), nonce, len(nonce), + plaintext, len(plaintext), siv, ciphertext) + if ret < 0: # pragma: no cover + raise WolfCryptError("AES-SIV encryption error (%d)" % ret) + return _ffi.buffer(siv)[:], _ffi.buffer(ciphertext)[:] + + def decrypt(self, associated_data, nonce, siv, ciphertext): + """ + Decrypt the ciphertext using the nonce and SIV provided. + The integrity of the associated data is checked. + + Associated data may be provided as single str or bytes, or as a + list of str or bytes in case of multiple blocks. + + Returns the decrypted plaintext. + """ + # Prepare the associated data blocks. Make sure to hold on to the + # returned references until the C function has been called in order + # to prevent garbage collection of them until the function is done. + associated_data, _refs = ( + AesSiv._prepare_associated_data(associated_data)) + nonce = t2b(nonce) + siv = t2b(siv) + if len(siv) != AesSiv.block_size: + raise ValueError("SIV must be %s in length, not %d" % + (AesSiv.block_size, len(siv))) + ciphertext = t2b(ciphertext) + plaintext = _ffi.new("byte[%d]" % len(ciphertext)) + ref = _lib.wc_AesSivDecrypt_ex(self._key, len(self._key), + associated_data, len(associated_data), nonce, len(nonce), + ciphertext, len(ciphertext), siv, plaintext) + if ref < 0: + raise WolfCryptError("AES-SIV decryption error (%d)" % ref) + return _ffi.buffer(plaintext)[:] + + @staticmethod + def _prepare_associated_data(associated_data): + """ + Prepare associated data for sending to C library. + + Associated data may be provided as single str or bytes, or as a + list of str or bytes in case of multiple blocks. + + The result is a tuple of the list of cffi cdata pointers to + AesSivAssoc structures, as well as the converted associated + data blocks. The caller **must** hold on to these until the + C function has been called, in order to make sure that the memory + is not freed by the FFI garbage collector before the data is read. + """ + if (isinstance(associated_data, str) or isinstance(associated_data, bytes)): + # A single block is provided. + # Make sure we have bytes. + associated_data = t2b(associated_data) + result = _ffi.new("AesSivAssoc[1]") + result[0].assoc = _ffi.from_buffer(associated_data) + result[0].assocSz = len(associated_data) + else: + # It is assumed that a list is provided. + num_blocks = len(associated_data) + if (num_blocks > 126): + raise WolfCryptError("AES-SIV does not support more than 126 blocks " + "of associated data, got: %d" % num_blocks) + # Make sure we have bytes. + associated_data = [t2b(block) for block in associated_data] + result = _ffi.new("AesSivAssoc[]", num_blocks) + for index, block in enumerate(associated_data): + result[index].assoc = _ffi.from_buffer(block) + result[index].assocSz = len(block) + # Return the converted associated data blocks so the caller can + # hold on to them until the function has been called. + return result, associated_data + + if _lib.AESGCM_STREAM_ENABLED: class AesGcmStream(object): """