Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions scripts/build_ffi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"]) + """;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 += """
Expand Down Expand Up @@ -1000,6 +1023,7 @@ def main(ffibuilder):
"SHA3": 1,
"DES3": 1,
"AES": 1,
"AES_SIV": 1,
"HMAC": 1,
"RSA": 1,
"RSA_BLINDING": 1,
Expand Down
145 changes: 144 additions & 1 deletion tests/test_ciphers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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
107 changes: 107 additions & 0 deletions wolfcrypt/ciphers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down