Skip to content
Open
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
91 changes: 91 additions & 0 deletions tests/test_chacha_iv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
#
# test_chacha_iv.py
#
# Copyright (C) 2006-2022 wolfSSL Inc.
#
# This file is part of wolfSSL. (formerly known as CyaSSL)
#
# wolfSSL is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# wolfSSL is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA

# pylint: disable=missing-docstring, import-error

import pytest
from wolfcrypt._ffi import lib as _lib
from wolfcrypt.exceptions import WolfCryptError

pytestmark = pytest.mark.skipif(
not _lib.CHACHA_ENABLED, reason="ChaCha not enabled")

KEY = b"\x01" * 32
NONCE = b"\x02" * 12


def test_encrypt_before_set_iv_raises():
"""
F-4463: encrypt() before set_iv() must not feed an empty IV buffer to
wc_Chacha_SetIV (which unconditionally reads 12 bytes). It must raise.
"""
from wolfcrypt.ciphers import ChaCha

cipher = ChaCha(KEY)
with pytest.raises(WolfCryptError):
cipher.encrypt(b"A" * 16)


def test_decrypt_before_set_iv_raises():
from wolfcrypt.ciphers import ChaCha

cipher = ChaCha(KEY)
with pytest.raises(WolfCryptError):
cipher.decrypt(b"A" * 16)


def test_encrypt_decrypt_after_set_iv_roundtrips():
from wolfcrypt.ciphers import ChaCha

enc = ChaCha(KEY)
enc.set_iv(NONCE)
plaintext = b"the quick brown fox"
ciphertext = enc.encrypt(plaintext)

dec = ChaCha(KEY)
dec.set_iv(NONCE)
assert dec.decrypt(ciphertext) == plaintext


def test_failed_set_iv_keeps_encrypt_blocked(monkeypatch):
"""
If re-keying fails inside set_iv(), the IV must be treated as not set so
encrypt()/decrypt() stay blocked rather than running with a stale or
partially-applied IV.
"""
from wolfcrypt.ciphers import ChaCha

cipher = ChaCha(KEY)
# First, establish a valid IV so a later failure would otherwise leave
# _iv_set True under the old ordering.
cipher.set_iv(NONCE)

monkeypatch.setattr(cipher, "_set_key", lambda direction: -1)
with pytest.raises(WolfCryptError):
cipher.set_iv(NONCE)
monkeypatch.undo() # restore real _set_key

# The failed re-key must have cleared the "IV is set" state, so encrypt()
# refuses here. Under the old ordering _iv_set stayed True and this
# encrypt() would instead run with a stale IV.
with pytest.raises(WolfCryptError):
cipher.encrypt(b"A" * 16)
59 changes: 59 additions & 0 deletions tests/test_cipher_modes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
#
# test_cipher_modes.py
#
# Copyright (C) 2006-2022 wolfSSL Inc.
#
# This file is part of wolfSSL. (formerly known as CyaSSL)
#
# wolfSSL is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# wolfSSL is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA

# pylint: disable=missing-docstring, import-error, protected-access

import pytest
from wolfcrypt.ciphers import (
_FEEDBACK_MODES,
MODE_CBC, MODE_CTR, MODE_ECB, MODE_CFB, MODE_OFB,
)
from wolfcrypt._ffi import lib as _lib


def test_feedback_modes_only_advertises_supported():
"""
F-4015: _FEEDBACK_MODES is used as the "supported modes" gate in
_Cipher.__init__. It must not advertise modes that the constructor
then turns around and rejects.
"""
assert MODE_CBC in _FEEDBACK_MODES
assert MODE_CTR in _FEEDBACK_MODES
for unsupported in (MODE_ECB, MODE_CFB, MODE_OFB):
assert unsupported not in _FEEDBACK_MODES


@pytest.mark.skipif(not _lib.AES_ENABLED, reason="AES not enabled")
def test_unsupported_mode_gives_single_consistent_error():
"""
F-4015: previously MODE_ECB passed the first "is supported" check and
then hit a contradictory "not supported by this cipher" branch. The
rejection must now be a single, consistent message.
"""
from wolfcrypt.ciphers import Aes

key = b"0" * 16
iv = b"0" * 16
with pytest.raises(ValueError) as exc_info:
Aes.new(key, MODE_ECB, iv)

assert "by this cipher" not in str(exc_info.value)
23 changes: 16 additions & 7 deletions tests/test_hashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,18 +172,24 @@ def test_hash(hash_cls, vectors):
assert hash_obj.hexdigest() == digest

# copy
hash_obj = hash_new(hash_cls)
copy = hash_obj.copy()
if hash_cls in hmac_params:
# HMAC copy is intentionally unsupported (see F-5428): wolfCrypt has
# no safe copy and byte-copying would alias the original's C state.
with pytest.raises(NotImplementedError):
hash_new(hash_cls).copy()
else:
hash_obj = hash_new(hash_cls)
copy = hash_obj.copy()

assert hash_obj.hexdigest() == copy.hexdigest()
assert hash_obj.hexdigest() == copy.hexdigest()

hash_obj.update("wolfcrypt")
hash_obj.update("wolfcrypt")

assert hash_obj.hexdigest() != copy.hexdigest()
assert hash_obj.hexdigest() != copy.hexdigest()

copy.update("wolfcrypt")
copy.update("wolfcrypt")

assert hash_obj.hexdigest() == copy.hexdigest() == digest
assert hash_obj.hexdigest() == copy.hexdigest() == digest


def test_hash_repeated_construction_destruction(hash_cls, vectors):
Expand All @@ -198,6 +204,9 @@ def test_hash_repeated_construction_destruction(hash_cls, vectors):

def test_hash_copy_destroy_lifecycle(hash_cls, vectors):
import gc
if hash_cls in hmac_params:
# HMAC does not support copy() (see F-5428).
pytest.skip("HMAC copy is not supported")
digest = vectors[hash_cls].digest
for _ in range(100):
h = hash_new(hash_cls, "wolfcrypt")
Expand Down
59 changes: 59 additions & 0 deletions tests/test_hmac_copy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
#
# test_hmac_copy.py
#
# Copyright (C) 2006-2022 wolfSSL Inc.
#
# This file is part of wolfSSL. (formerly known as CyaSSL)
#
# wolfSSL is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# wolfSSL is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA

# pylint: disable=missing-docstring, import-error

import pytest
from wolfcrypt._ffi import lib as _lib

pytestmark = pytest.mark.skipif(
not (_lib.HMAC_ENABLED and _lib.SHA256_ENABLED),
reason="HMAC-SHA256 not enabled")

KEY = b"wolfCrypt is the best crypto around"


def test_hmac_copy_raises_not_implemented():
"""
F-5428: _Hmac inherited _Hash.copy(), which for HMAC fell back to a
byte-level memmove and returned an object aliasing the original's C
state (use-after-free risk in async/HW builds). wolfCrypt has no safe
public copy, so HMAC copy() must refuse rather than alias.
"""
from wolfcrypt.hashes import HmacSha256

hmac = HmacSha256.new(KEY, b"some message")
with pytest.raises(NotImplementedError):
hmac.copy()


def test_hmac_digest_unaffected_by_copy_removal():
"""digest()/hexdigest() must keep working and remain repeatable."""
from wolfcrypt.hashes import HmacSha256

hmac = HmacSha256.new(KEY, b"some message")
first = hmac.hexdigest()
second = hmac.hexdigest()
assert first == second

hmac.update(b" more")
assert hmac.hexdigest() != first
49 changes: 34 additions & 15 deletions wolfcrypt/ciphers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
MODE_OFB = 5 # Output Feedback
MODE_CTR = 6 # Counter

_FEEDBACK_MODES = [MODE_ECB, MODE_CBC, MODE_CFB, MODE_OFB, MODE_CTR]
# Only the modes the generic _Cipher actually supports. MODE_ECB/MODE_CFB/
# MODE_OFB are defined above for PEP 272 completeness but are not implemented.
_FEEDBACK_MODES = [MODE_CBC, MODE_CTR]
Comment thread
julek-wolfssl marked this conversation as resolved.

# ECC curve id
ECC_CURVE_INVALID = -1
Expand Down Expand Up @@ -120,11 +122,9 @@ def __init__(self, key, mode, IV=None):
if mode not in _FEEDBACK_MODES:
raise ValueError("this mode is not supported")

if mode == MODE_CBC or mode == MODE_CTR:
if IV is None:
raise ValueError("this mode requires an 'IV' string")
else:
raise ValueError("this mode is not supported by this cipher")
# Both supported modes (CBC, CTR) require an IV / initial counter.
if IV is None:
raise ValueError("this mode requires an 'IV' string")

self.mode = mode

Expand Down Expand Up @@ -159,12 +159,11 @@ def new(cls, key, mode, IV=None, **kwargs): # pylint: disable=W0613
"""
Returns a ciphering object, using the secret key contained in
the string **key**, and using the feedback mode **mode**, which
must be one of MODE_* defined in this module.
must be one of the supported MODE_* values (MODE_CBC, MODE_CTR).

If **mode** is MODE_CBC or MODE_CFB, **IV** must be provided and
must be a string of the same length as the block size. Not
providing a value of **IV** will result in a ValueError exception
being raised.
Both supported modes require **IV** to be provided as a string of
the same length as the block size. Not providing a value of **IV**
will result in a ValueError exception being raised.
"""
return cls(key, mode, IV)

Expand All @@ -173,8 +172,9 @@ def encrypt(self, string):
Encrypts a non-empty string, using the key-dependent data in
the object, and with the appropriate feedback mode.

The string's length must be an exact multiple of the algorithm's
block size or, in CFB mode, of the segment size.
In MODE_CBC the string's length must be an exact multiple of the
algorithm's block size. MODE_CTR is a stream mode and imposes no
length restriction.

Returns a string containing the ciphertext.
"""
Expand Down Expand Up @@ -205,8 +205,9 @@ def decrypt(self, string):
Decrypts **string**, using the key-dependent data in the
object and with the appropriate feedback mode.

The string's length must be an exact multiple of the algorithm's
block size or, in CFB mode, of the segment size.
In MODE_CBC the string's length must be an exact multiple of the
algorithm's block size. MODE_CTR is a stream mode and imposes no
length restriction.

Returns a string containing the plaintext.
"""
Expand Down Expand Up @@ -523,6 +524,22 @@ def __init__(self, key="", size=32): # pylint: disable=unused-argument
self.key_size = len(self._key)
self._IV_nonce = b""
self._IV_counter = 0
# ChaCha takes no IV at construction; set_iv() must be called
# before any encrypt()/decrypt() so a real nonce is available.
self._iv_set = False

def encrypt(self, string):
self._require_iv()
return super().encrypt(string)

def decrypt(self, string):
self._require_iv()
return super().decrypt(string)

def _require_iv(self):
if not self._iv_set:
raise WolfCryptError(
"set_iv() must be called before encrypt()/decrypt()")

# Sentinel for "rekey both contexts" used by set_iv. Must not
# collide with _ENCRYPTION (0) or _DECRYPTION (1).
Expand Down Expand Up @@ -567,9 +584,11 @@ def set_iv(self, nonce, counter = 0):
if len(self._IV_nonce) != self._NONCE_SIZE:
raise ValueError(f"nonce must be {self._NONCE_SIZE} bytes, got {len(self._IV_nonce)}")
self._IV_counter = counter
self._iv_set = False
ret = self._set_key(self._REKEY_BOTH)
if ret < 0:
raise WolfCryptApiError("ChaCha set_iv error", ret)
self._iv_set = True

if _lib.CHACHA20_POLY1305_ENABLED:
class ChaCha20Poly1305:
Expand Down
17 changes: 10 additions & 7 deletions wolfcrypt/hashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,19 +392,22 @@ class _Hmac(_Hash):
A **PEP 247: Cryptographic Hash Functions** compliant
**Keyed Hash Function Interface**.

Note: wolfSSL does not provide a `wc_HmacCopy` equivalent, so
`copy()` falls back to a byte-level memmove. In default builds the
Hmac struct is self-contained and this is safe. In async or
hardware-accelerated builds where the struct contains internal
pointers, the copy shares those pointers with the original; the
copy must not outlive the original or be used after the original
is freed.
Note: wolfCrypt provides no safe public copy for an `Hmac` object.
Byte-copying the struct would alias the original's internal state
(e.g. async/hardware-accelerated builds keep pointers there), so
`copy()` is not supported and raises `NotImplementedError`.
"""
digest_size = None
_native_type = "Hmac *"
_native_size = _ffi.sizeof("Hmac")
_delete = staticmethod(_lib.wc_HmacFree)

def copy(self):
raise NotImplementedError(
"HMAC objects cannot be safely copied: wolfCrypt has no "
"wc_HmacCopy and byte-copying the state would alias the "
"original's internal C resources")

def __del__(self):
if hasattr(self, '_native_object') and not getattr(self, '_shallow_copy', False):
self._delete(self._native_object)
Expand Down
Loading