Skip to content

Commit 56e7814

Browse files
committed
Add AES-SIV support (single block only)
1 parent 0f2ceea commit 56e7814

File tree

3 files changed

+221
-0
lines changed

3 files changed

+221
-0
lines changed

scripts/build_ffi.py

Lines changed: 21 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,22 @@ 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;
655+
int wc_AesSivEncrypt(const byte* key, word32 keySz, const byte* assoc,
656+
word32 assocSz, const byte* nonce, word32 nonceSz,
657+
const byte* in, word32 inSz, byte* siv, byte* out);
658+
int wc_AesSivDecrypt(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_AesSivEncrypt_ex(const byte* key, word32 keySz, const AesSivAssoc* assoc,
662+
word32 numAssoc, const byte* nonce, word32 nonceSz,
663+
const byte* in, word32 inSz, byte* siv, byte* out);
664+
int wc_AesSivDecrypt_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+
"""
648668

649669
if features["CHACHA"]:
650670
cdef += """
@@ -1000,6 +1020,7 @@ def main(ffibuilder):
10001020
"SHA3": 1,
10011021
"DES3": 1,
10021022
"AES": 1,
1023+
"AES_SIV": 1,
10031024
"HMAC": 1,
10041025
"RSA": 1,
10051026
"RSA_BLINDING": 1,

tests/test_ciphers.py

Lines changed: 147 additions & 0 deletions
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

@@ -727,3 +731,146 @@ 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+
@pytest.mark.skip(reason=("Associated data in test vector consists of "
782+
"multiple blocks which is unsupported"))
783+
def test_aessiv_encrypt_kat_rfc5297():
784+
"""
785+
Known-answer test using test vectors from RFC-5297.
786+
"""
787+
aessiv = AesSiv(TEST_VECTOR_KEY_RFC5297)
788+
# This is probably not the correct way of handling the associated data.
789+
# The function wc_AesSivEncrypt_ex supports this but it is currently not
790+
# exposed.
791+
associated_data = (
792+
TEST_VECTOR_ASSOCIATED_DATA_1_RFC5297
793+
+ TEST_VECTOR_ASSOCIATED_DATA_2_RFC5297
794+
)
795+
siv, ciphertext = aessiv.encrypt(
796+
associated_data,
797+
TEST_VECTOR_NONCE_RFC5297,
798+
TEST_VECTOR_PLAINTEXT_RFC5297
799+
)
800+
assert siv == TEST_VECTOR_SIV_RFC5297
801+
assert ciphertext == TEST_VECTOR_CIPHERTEXT_RFC5297
802+
803+
804+
@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled")
805+
@pytest.mark.skip(reason=("Associated data in test vector consists "
806+
"of multiple blocks which is unsupported"))
807+
def test_aessiv_decrypt_kat_rfc5297():
808+
"""
809+
Known-answer test using test vectors from RFC-5297.
810+
"""
811+
aessiv = AesSiv(TEST_VECTOR_KEY_RFC5297)
812+
# This is probably not the correct way of handling the associated data.
813+
# The function wc_AesSivEncrypt_ex supports this but it is currently not
814+
# exposed.
815+
associated_data = (
816+
TEST_VECTOR_ASSOCIATED_DATA_1_RFC5297
817+
+ TEST_VECTOR_ASSOCIATED_DATA_2_RFC5297
818+
)
819+
plaintext = aessiv.decrypt(
820+
associated_data,
821+
TEST_VECTOR_NONCE_RFC5297,
822+
TEST_VECTOR_SIV_RFC5297,
823+
TEST_VECTOR_CIPHERTEXT_RFC5297
824+
)
825+
assert plaintext == TEST_VECTOR_PLAINTEXT_RFC5297
826+
827+
828+
#
829+
# Test vectors copied from OpenSSL library file evpciph_aes_siv.txt..
830+
#
831+
TEST_VECTOR_KEY_OPENSSL = bytes.fromhex(
832+
"fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
833+
)
834+
TEST_VECTOR_ASSOCIATED_DATA_OPENSSL = bytes.fromhex(
835+
"101112131415161718191a1b1c1d1e1f2021222324252627"
836+
)
837+
TEST_VECTOR_NONCE_OPENSSL = b""
838+
TEST_VECTOR_PLAINTEXT_OPENSSL = bytes.fromhex(
839+
"112233445566778899aabbccddee"
840+
)
841+
TEST_VECTOR_SIV_OPENSSL = bytes.fromhex(
842+
"85632d07c6e8f37f950acd320a2ecc93"
843+
)
844+
TEST_VECTOR_CIPHERTEXT_OPENSSL = bytes.fromhex(
845+
"40c02b9690c4dc04daef7f6afe5c"
846+
)
847+
848+
849+
@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled")
850+
def test_aessiv_encrypt_kat_openssl():
851+
"""
852+
Known-answer test using test vectors from OpenSSL.
853+
"""
854+
aessiv = AesSiv(TEST_VECTOR_KEY_OPENSSL)
855+
siv, ciphertext = aessiv.encrypt(
856+
TEST_VECTOR_ASSOCIATED_DATA_OPENSSL,
857+
TEST_VECTOR_NONCE_OPENSSL,
858+
TEST_VECTOR_PLAINTEXT_OPENSSL
859+
)
860+
assert siv == TEST_VECTOR_SIV_OPENSSL
861+
assert ciphertext == TEST_VECTOR_CIPHERTEXT_OPENSSL
862+
863+
864+
@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled")
865+
def test_aessiv_decrypt_kat_openssl():
866+
"""
867+
Known-answer test using test vectors from OpenSSL.
868+
"""
869+
aessiv = AesSiv(TEST_VECTOR_KEY_OPENSSL)
870+
plaintext = aessiv.decrypt(
871+
TEST_VECTOR_ASSOCIATED_DATA_OPENSSL,
872+
TEST_VECTOR_NONCE_OPENSSL,
873+
TEST_VECTOR_SIV_OPENSSL,
874+
TEST_VECTOR_CIPHERTEXT_OPENSSL
875+
)
876+
assert plaintext == TEST_VECTOR_PLAINTEXT_OPENSSL

wolfcrypt/ciphers.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,59 @@ 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+
Returns a tuple of the IV and ciphertext.
298+
"""
299+
associated_data = t2b(associated_data)
300+
nonce = t2b(nonce)
301+
plaintext = t2b(plaintext)
302+
siv = _ffi.new("byte[%d]" % AesSiv.block_size)
303+
ciphertext = _ffi.new("byte[%d]" % len(plaintext))
304+
ret = _lib.wc_AesSivEncrypt(self._key, len(self._key), associated_data, len(associated_data),
305+
nonce, len(nonce), plaintext, len(plaintext), siv, ciphertext)
306+
if ret < 0: # pragma: no cover
307+
raise WolfCryptError("AES-SIV encryption error (%d)" % ret)
308+
return _ffi.buffer(siv)[:], _ffi.buffer(ciphertext)[:]
309+
310+
def decrypt(self, associated_data, nonce, siv, ciphertext):
311+
"""
312+
Decrypt the ciphertext using the nonce and SIV provided.
313+
The integrity of the associated data is checked.
314+
315+
Returns the decrypted plaintext.
316+
"""
317+
associated_data = t2b(associated_data)
318+
nonce = t2b(nonce)
319+
siv = t2b(siv)
320+
if len(siv) != AesSiv.block_size:
321+
raise ValueError("SIV must be %s in length, not %d" %
322+
(AesSiv.block_size, len(siv)))
323+
ciphertext = t2b(ciphertext)
324+
plaintext = _ffi.new("byte[%d]" % len(ciphertext))
325+
ref = _lib.wc_AesSivDecrypt(self._key, len(self._key), associated_data, len(associated_data),
326+
nonce, len(nonce), ciphertext, len(ciphertext), siv, plaintext)
327+
if ref < 0:
328+
raise WolfCryptError("AES-SIV decryption error (%d)" % ref)
329+
return _ffi.buffer(plaintext)[:]
330+
278331
if _lib.AESGCM_STREAM_ENABLED:
279332
class AesGcmStream(object):
280333
"""

0 commit comments

Comments
 (0)