Skip to content

Commit 99aed59

Browse files
authored
Merge pull request #74 from mjdemilliano/aessiv-full
Add AES-SIV support
2 parents 0f2ceea + 523b36b commit 99aed59

File tree

3 files changed

+275
-1
lines changed

3 files changed

+275
-1
lines changed

scripts/build_ffi.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ def make_flags(prefix, fips):
205205
flags.append("--enable-aesgcm-stream")
206206

207207
flags.append("--enable-aesgcm")
208+
flags.append("--enable-aessiv")
208209

209210
# hashes and MACs
210211
flags.append("--enable-sha")
@@ -358,6 +359,7 @@ def get_features(local_wolfssl, features):
358359
features["SHA3"] = 1 if '#define WOLFSSL_SHA3' in defines else 0
359360
features["DES3"] = 0 if '#define NO_DES3' in defines else 1
360361
features["AES"] = 0 if '#define NO_AES' in defines else 1
362+
features["AES_SIV"] = 1 if '#define WOLFSSL_AES_SIV' in defines else 0
361363
features["CHACHA"] = 1 if '#define HAVE_CHACHA' in defines else 0
362364
features["HMAC"] = 0 if '#define NO_HMAC' in defines else 1
363365
features["RSA"] = 0 if '#define NO_RSA' in defines else 1
@@ -472,6 +474,7 @@ def build_ffi(local_wolfssl, features):
472474
int SHA3_ENABLED = """ + str(features["SHA3"]) + """;
473475
int DES3_ENABLED = """ + str(features["DES3"]) + """;
474476
int AES_ENABLED = """ + str(features["AES"]) + """;
477+
int AES_SIV_ENABLED = """ + str(features["AES_SIV"]) + """;
475478
int CHACHA_ENABLED = """ + str(features["CHACHA"]) + """;
476479
int HMAC_ENABLED = """ + str(features["HMAC"]) + """;
477480
int RSA_ENABLED = """ + str(features["RSA"]) + """;
@@ -509,6 +512,7 @@ def build_ffi(local_wolfssl, features):
509512
extern int SHA3_ENABLED;
510513
extern int DES3_ENABLED;
511514
extern int AES_ENABLED;
515+
extern int AES_SIV_ENABLED;
512516
extern int CHACHA_ENABLED;
513517
extern int HMAC_ENABLED;
514518
extern int RSA_ENABLED;
@@ -645,6 +649,25 @@ def build_ffi(local_wolfssl, features):
645649
word32 authTagSz);
646650
"""
647651

652+
if features["AES"] and features["AES_SIV"]:
653+
cdef += """
654+
typedef struct AesSivAssoc_s {
655+
const byte* assoc;
656+
word32 assocSz;
657+
} AesSivAssoc;
658+
int wc_AesSivEncrypt(const byte* key, word32 keySz, const byte* assoc,
659+
word32 assocSz, const byte* nonce, word32 nonceSz,
660+
const byte* in, word32 inSz, byte* siv, byte* out);
661+
int wc_AesSivDecrypt(const byte* key, word32 keySz, const byte* assoc,
662+
word32 assocSz, const byte* nonce, word32 nonceSz,
663+
const byte* in, word32 inSz, byte* siv, byte* out);
664+
int wc_AesSivEncrypt_ex(const byte* key, word32 keySz, const AesSivAssoc* assoc,
665+
word32 numAssoc, const byte* nonce, word32 nonceSz,
666+
const byte* in, word32 inSz, byte* siv, byte* out);
667+
int wc_AesSivDecrypt_ex(const byte* key, word32 keySz, const AesSivAssoc* assoc,
668+
word32 numAssoc, const byte* nonce, word32 nonceSz,
669+
const byte* in, word32 inSz, byte* siv, byte* out);
670+
"""
648671

649672
if features["CHACHA"]:
650673
cdef += """
@@ -1000,6 +1023,7 @@ def main(ffibuilder):
10001023
"SHA3": 1,
10011024
"DES3": 1,
10021025
"AES": 1,
1026+
"AES_SIV": 1,
10031027
"HMAC": 1,
10041028
"RSA": 1,
10051029
"RSA_BLINDING": 1,

tests/test_ciphers.py

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# pylint: disable=redefined-outer-name
2222

2323
from collections import namedtuple
24+
import random
2425
import pytest
2526
from wolfcrypt._ffi import ffi as _ffi
2627
from wolfcrypt._ffi import lib as _lib
@@ -35,6 +36,9 @@
3536
if _lib.AES_ENABLED:
3637
from wolfcrypt.ciphers import Aes
3738

39+
if _lib.AES_SIV_ENABLED:
40+
from wolfcrypt.ciphers import AesSiv
41+
3842
if _lib.CHACHA_ENABLED:
3943
from wolfcrypt.ciphers import ChaCha
4044

@@ -57,7 +61,7 @@
5761

5862
@pytest.fixture
5963
def vectors():
60-
TestVector = namedtuple("TestVector", """key iv plaintext ciphertext
64+
TestVector = namedtuple("TestVector", """key iv plaintext ciphertext
6165
ciphertext_ctr raw_key
6266
pkcs8_key pem""")
6367
TestVector.__new__.__defaults__ = (None,) * len(TestVector._fields)
@@ -727,3 +731,142 @@ def test_ed448_sign_verify(ed448_private, ed448_public):
727731
# private object holds both private and public info, so it can also verify
728732
# using the known public key.
729733
assert ed448_private.verify(signature, plaintext)
734+
735+
736+
@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled")
737+
def test_aessiv_encrypt_decrypt():
738+
key = random.randbytes(32)
739+
aessiv = AesSiv(key)
740+
associated_data = random.randbytes(16)
741+
nonce = random.randbytes(12)
742+
plaintext = random.randbytes(16)
743+
siv, ciphertext = aessiv.encrypt(associated_data, nonce, plaintext)
744+
assert aessiv.decrypt(associated_data, nonce, siv, ciphertext) == plaintext
745+
746+
747+
#
748+
# Test vectors copied from RFC-5297.
749+
#
750+
TEST_VECTOR_KEY_RFC5297 = bytes.fromhex(
751+
"7f7e7d7c 7b7a7978 77767574 73727170"
752+
"40414243 44454647 48494a4b 4c4d4e4f"
753+
)
754+
TEST_VECTOR_ASSOCIATED_DATA_1_RFC5297 = bytes.fromhex(
755+
"00112233 44556677 8899aabb ccddeeff"
756+
"deaddada deaddada ffeeddcc bbaa9988"
757+
"77665544 33221100"
758+
)
759+
TEST_VECTOR_ASSOCIATED_DATA_2_RFC5297 = bytes.fromhex(
760+
"10203040 50607080 90a0"
761+
)
762+
TEST_VECTOR_NONCE_RFC5297 = bytes.fromhex(
763+
"09f91102 9d74e35b d84156c5 635688c0"
764+
)
765+
TEST_VECTOR_PLAINTEXT_RFC5297 = bytes.fromhex(
766+
"74686973 20697320 736f6d65 20706c61"
767+
"696e7465 78742074 6f20656e 63727970"
768+
"74207573 696e6720 5349562d 414553"
769+
)
770+
TEST_VECTOR_SIV_RFC5297 = bytes.fromhex(
771+
"7bdb6e3b 432667eb 06f4d14b ff2fbd0f"
772+
)
773+
TEST_VECTOR_CIPHERTEXT_RFC5297 = bytes.fromhex(
774+
"cb900f2f ddbe4043 26601965 c889bf17"
775+
"dba77ceb 094fa663 b7a3f748 ba8af829"
776+
"ea64ad54 4a272e9c 485b62a3 fd5c0d"
777+
)
778+
779+
780+
@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled")
781+
def test_aessiv_encrypt_kat_rfc5297():
782+
"""
783+
Known-answer test using test vectors from RFC-5297.
784+
"""
785+
aessiv = AesSiv(TEST_VECTOR_KEY_RFC5297)
786+
associated_data = [
787+
TEST_VECTOR_ASSOCIATED_DATA_1_RFC5297,
788+
TEST_VECTOR_ASSOCIATED_DATA_2_RFC5297,
789+
]
790+
siv, ciphertext = aessiv.encrypt(
791+
associated_data,
792+
TEST_VECTOR_NONCE_RFC5297,
793+
TEST_VECTOR_PLAINTEXT_RFC5297
794+
)
795+
assert siv == TEST_VECTOR_SIV_RFC5297
796+
assert ciphertext == TEST_VECTOR_CIPHERTEXT_RFC5297
797+
798+
799+
@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled")
800+
def test_aessiv_decrypt_kat_rfc5297():
801+
"""
802+
Known-answer test using test vectors from RFC-5297.
803+
"""
804+
aessiv = AesSiv(TEST_VECTOR_KEY_RFC5297)
805+
associated_data = (
806+
TEST_VECTOR_ASSOCIATED_DATA_1_RFC5297,
807+
TEST_VECTOR_ASSOCIATED_DATA_2_RFC5297,
808+
)
809+
plaintext = aessiv.decrypt(
810+
associated_data,
811+
TEST_VECTOR_NONCE_RFC5297,
812+
TEST_VECTOR_SIV_RFC5297,
813+
TEST_VECTOR_CIPHERTEXT_RFC5297
814+
)
815+
assert plaintext == TEST_VECTOR_PLAINTEXT_RFC5297
816+
817+
818+
#
819+
# Test vectors copied from OpenSSL library file evpciph_aes_siv.txt..
820+
#
821+
TEST_VECTOR_KEY_OPENSSL = bytes.fromhex(
822+
"fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
823+
)
824+
TEST_VECTOR_ASSOCIATED_DATA_OPENSSL = bytes.fromhex(
825+
"101112131415161718191a1b1c1d1e1f2021222324252627"
826+
)
827+
TEST_VECTOR_NONCE_OPENSSL = b""
828+
TEST_VECTOR_PLAINTEXT_OPENSSL = bytes.fromhex(
829+
"112233445566778899aabbccddee"
830+
)
831+
TEST_VECTOR_SIV_OPENSSL = bytes.fromhex(
832+
"85632d07c6e8f37f950acd320a2ecc93"
833+
)
834+
TEST_VECTOR_CIPHERTEXT_OPENSSL = bytes.fromhex(
835+
"40c02b9690c4dc04daef7f6afe5c"
836+
)
837+
838+
839+
@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled")
840+
def test_aessiv_encrypt_kat_openssl():
841+
"""
842+
Known-answer test using test vectors from OpenSSL.
843+
844+
This also tests calling AesSiv with a single associated data block, not
845+
provided as a list of blocks.
846+
"""
847+
aessiv = AesSiv(TEST_VECTOR_KEY_OPENSSL)
848+
siv, ciphertext = aessiv.encrypt(
849+
TEST_VECTOR_ASSOCIATED_DATA_OPENSSL,
850+
TEST_VECTOR_NONCE_OPENSSL,
851+
TEST_VECTOR_PLAINTEXT_OPENSSL
852+
)
853+
assert siv == TEST_VECTOR_SIV_OPENSSL
854+
assert ciphertext == TEST_VECTOR_CIPHERTEXT_OPENSSL
855+
856+
857+
@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled")
858+
def test_aessiv_decrypt_kat_openssl():
859+
"""
860+
Known-answer test using test vectors from OpenSSL.
861+
862+
This also tests calling AesSiv with a single associated data block, not
863+
provided as a list of blocks.
864+
"""
865+
aessiv = AesSiv(TEST_VECTOR_KEY_OPENSSL)
866+
plaintext = aessiv.decrypt(
867+
TEST_VECTOR_ASSOCIATED_DATA_OPENSSL,
868+
TEST_VECTOR_NONCE_OPENSSL,
869+
TEST_VECTOR_SIV_OPENSSL,
870+
TEST_VECTOR_CIPHERTEXT_OPENSSL
871+
)
872+
assert plaintext == TEST_VECTOR_PLAINTEXT_OPENSSL

wolfcrypt/ciphers.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,113 @@ def _decrypt(self, destination, source):
275275
else:
276276
raise ValueError("Invalid mode associated to cipher")
277277

278+
if _lib.AES_SIV_ENABLED:
279+
class AesSiv(object):
280+
"""
281+
AES-SIV (Synthetic Initialization Vector) implementation as described in RFC 5297.
282+
"""
283+
_key_sizes = [16, 24, 32]
284+
block_size = 16
285+
286+
def __init__(self, key):
287+
self._key = t2b(key)
288+
if len(self._key) not in AesSiv._key_sizes:
289+
raise ValueError("key must be %s in length, not %d" %
290+
(AesSiv._key_sizes, len(self._key)))
291+
292+
def encrypt(self, associated_data, nonce, plaintext):
293+
"""
294+
Encrypt plaintext data using the nonce provided. The associated
295+
data is not encrypted but is included in the authentication tag.
296+
297+
Associated data may be provided as single str or bytes, or as a
298+
list of str or bytes in case of multiple blocks.
299+
300+
Returns a tuple of the IV and ciphertext.
301+
"""
302+
# Prepare the associated data blocks. Make sure to hold on to the
303+
# returned references until the C function has been called in order
304+
# to prevent garbage collection of them until the function is done.
305+
associated_data, _refs = (
306+
AesSiv._prepare_associated_data(associated_data))
307+
nonce = t2b(nonce)
308+
plaintext = t2b(plaintext)
309+
siv = _ffi.new("byte[%d]" % AesSiv.block_size)
310+
ciphertext = _ffi.new("byte[%d]" % len(plaintext))
311+
ret = _lib.wc_AesSivEncrypt_ex(self._key, len(self._key),
312+
associated_data, len(associated_data), nonce, len(nonce),
313+
plaintext, len(plaintext), siv, ciphertext)
314+
if ret < 0: # pragma: no cover
315+
raise WolfCryptError("AES-SIV encryption error (%d)" % ret)
316+
return _ffi.buffer(siv)[:], _ffi.buffer(ciphertext)[:]
317+
318+
def decrypt(self, associated_data, nonce, siv, ciphertext):
319+
"""
320+
Decrypt the ciphertext using the nonce and SIV provided.
321+
The integrity of the associated data is checked.
322+
323+
Associated data may be provided as single str or bytes, or as a
324+
list of str or bytes in case of multiple blocks.
325+
326+
Returns the decrypted plaintext.
327+
"""
328+
# Prepare the associated data blocks. Make sure to hold on to the
329+
# returned references until the C function has been called in order
330+
# to prevent garbage collection of them until the function is done.
331+
associated_data, _refs = (
332+
AesSiv._prepare_associated_data(associated_data))
333+
nonce = t2b(nonce)
334+
siv = t2b(siv)
335+
if len(siv) != AesSiv.block_size:
336+
raise ValueError("SIV must be %s in length, not %d" %
337+
(AesSiv.block_size, len(siv)))
338+
ciphertext = t2b(ciphertext)
339+
plaintext = _ffi.new("byte[%d]" % len(ciphertext))
340+
ref = _lib.wc_AesSivDecrypt_ex(self._key, len(self._key),
341+
associated_data, len(associated_data), nonce, len(nonce),
342+
ciphertext, len(ciphertext), siv, plaintext)
343+
if ref < 0:
344+
raise WolfCryptError("AES-SIV decryption error (%d)" % ref)
345+
return _ffi.buffer(plaintext)[:]
346+
347+
@staticmethod
348+
def _prepare_associated_data(associated_data):
349+
"""
350+
Prepare associated data for sending to C library.
351+
352+
Associated data may be provided as single str or bytes, or as a
353+
list of str or bytes in case of multiple blocks.
354+
355+
The result is a tuple of the list of cffi cdata pointers to
356+
AesSivAssoc structures, as well as the converted associated
357+
data blocks. The caller **must** hold on to these until the
358+
C function has been called, in order to make sure that the memory
359+
is not freed by the FFI garbage collector before the data is read.
360+
"""
361+
if (isinstance(associated_data, str) or isinstance(associated_data, bytes)):
362+
# A single block is provided.
363+
# Make sure we have bytes.
364+
associated_data = t2b(associated_data)
365+
result = _ffi.new("AesSivAssoc[1]")
366+
result[0].assoc = _ffi.from_buffer(associated_data)
367+
result[0].assocSz = len(associated_data)
368+
else:
369+
# It is assumed that a list is provided.
370+
num_blocks = len(associated_data)
371+
if (num_blocks > 126):
372+
raise WolfCryptError("AES-SIV does not support more than 126 blocks "
373+
"of associated data, got: %d" % num_blocks)
374+
# Make sure we have bytes.
375+
associated_data = [t2b(block) for block in associated_data]
376+
result = _ffi.new("AesSivAssoc[]", num_blocks)
377+
for index, block in enumerate(associated_data):
378+
result[index].assoc = _ffi.from_buffer(block)
379+
result[index].assocSz = len(block)
380+
# Return the converted associated data blocks so the caller can
381+
# hold on to them until the function has been called.
382+
return result, associated_data
383+
384+
278385
if _lib.AESGCM_STREAM_ENABLED:
279386
class AesGcmStream(object):
280387
"""

0 commit comments

Comments
 (0)