Add typing: asn, exceptions, hashes, pwdbased, utils.#125
Add typing: asn, exceptions, hashes, pwdbased, utils.#125roberthdevries wants to merge 3 commits into
Conversation
65d14db to
1c2b082
Compare
|
This also adds the |
dgarske
left a comment
There was a problem hiding this comment.
Skoll Code Review
Scan type: reviewOverall recommendation: REQUEST_CHANGES
Findings: 7 total — 7 posted, 0 skipped
6 finding(s) posted as inline comments (see file-level comments below)
Posted findings
- [High] AES-SIV single-block associated-data length uses char count of original input, not encoded byte length —
wolfcrypt/ciphers.py:397-400 - [Medium] sign_with_seed no longer accepts bytearray/memoryview seeds (regression) —
wolfcrypt/ciphers.py:2513-2546 - [Medium] make_key_from_seed now silently UTF-8-encodes a str seed instead of rejecting it —
wolfcrypt/ciphers.py:2360-2367 - [Low] ChaCha init renamed size to _size, breaking the documented backward-compatible keyword —
wolfcrypt/ciphers.py:544 - [Low] HKDF helpers annotate hash_cls as instance type instead of class type —
wolfcrypt/hkdf.py:33,78,105 - [Low] Random no longer nulls native_object on init failure; del frees an uninitialized RNG —
wolfcrypt/random.py:37-52 - [Low] RsaPublic.init made key a required positional argument —
wolfcrypt/ciphers.py:771-774
Review generated by Skoll
| # A single block is provided. | ||
| # Make sure we have bytes. | ||
| associated_data = t2b(associated_data) | ||
| associated_data_bytes = t2b(associated_data) |
There was a problem hiding this comment.
🔴 [High] AES-SIV single-block associated-data length uses char count of original input, not encoded byte length
The refactor renamed the local variable but left the assocSz assignment pointing at the original input. associated_data_bytes = t2b(associated_data) creates a NEW bytes object, while line 400 still does result[0].assocSz = len(associated_data) against the ORIGINAL associated_data. Before this PR the code reassigned associated_data = t2b(associated_data), so len(associated_data) was the encoded byte length and was correct. Now, when associated_data is a str (an explicitly documented single-block input type — see the docstring "a single str, bytes, bytearray, or memoryview") containing non-ASCII/multibyte UTF-8 characters, len(associated_data) is the character count, which is smaller than len(associated_data_bytes) (the UTF-8 byte count). result[0].assoc points at the full encoded byte buffer but assocSz is set too small, so wc_AesSivEncrypt_ex/wc_AesSivDecrypt_ex authenticate only a truncated prefix of the associated data. The library's own encrypt/decrypt round-trip will still pass (both sides truncate identically), masking the defect, but the tag will not match any spec-correct implementation and part of the AD goes unauthenticated. The list branch (line 412) correctly uses len(block) over associated_data_bytes, so only the single-block branch is affected.
Fix: Change line 400 to use len(associated_data_bytes) so the authenticated length matches the buffer actually pointed to. Add a test with a non-ASCII str single-block associated data (e.g. compare a str AD against the equivalent UTF-8 bytes AD) to lock in the fix.
| "seed must support the buffer protocol, such as `bytes` or `bytearray`" | ||
| ) from exception | ||
| if len(seed_view) != ML_DSA_SIGNATURE_SEED_LENGTH: | ||
| if len(seed) != ML_DSA_SIGNATURE_SEED_LENGTH: |
There was a problem hiding this comment.
🟠 [Medium] sign_with_seed no longer accepts bytearray/memoryview seeds (regression)
The PR removed the memoryview(seed) + _ffi.from_buffer(seed_view) handling and now passes the raw seed object straight into wc_dilithium_sign_msg_with_seed/wc_dilithium_sign_ctx_msg_with_seed. The rest of this codebase consistently converts inputs to bytes (via t2b) or wraps them with _ffi.from_buffer before handing them to cffi, which strongly implies cffi accepts a plain bytes object for the const byte* argument but not a bytearray/memoryview. Under the old code a bytearray or memoryview seed of the correct length worked; now it will raise a raw cffi TypeError. This is a behavioral regression for buffer-protocol seed inputs, and the previous friendly error message ("seed must support the buffer protocol, such as bytes or bytearray") is also lost. The updated test only verifies that a str raises TypeError; there is no test for bytearray/memoryview seeds.
Fix: Restore buffer-protocol handling so bytes/bytearray/memoryview seeds all work while still rejecting str (e.g. bytes(memoryview(seed))). Add a parametrized test covering bytearray and memoryview seeds.
There was a problem hiding this comment.
After a bit of reverse engineering the set of accepted types are actually bytes, list/tuple of int (where each int should be in the range of values that a byte can have).
This is actually checked by the automatically generated bindings of cffi, and those bindings will perform the checks and will raise a TypeError if you pass a string for example.
This removes the need to check explicitly and aligns the accepted types of this method with the accepted types of the underlying call to wc_dilithium_sign_ctx_msg_with_seed
| @@ -2316,35 +2358,29 @@ def make_key_from_seed(cls, mldsa_type, seed): | |||
| :type seed: bytes | |||
| """ | |||
| mldsa_priv = cls(mldsa_type) | |||
There was a problem hiding this comment.
🟠 [Medium] make_key_from_seed now silently UTF-8-encodes a str seed instead of rejecting it
The PR replaced the memoryview(seed) buffer-protocol check with seed_bytes = t2b(seed). t2b accepts str and encodes it as UTF-8, so a str seed (previously rejected with a TypeError because memoryview(str) fails) is now silently accepted and used as deterministic key material. This is a footgun for deterministic key generation and is inconsistent with the sibling sign_with_seed, which (per the updated test) now rejects str seeds with a TypeError. The two seed-consuming APIs should agree on whether str is a valid seed type.
Fix: Decide on one seed-type policy and apply it to both make_key_from_seed and sign_with_seed (recommend: accept bytes-like, reject str). Add a test asserting str seeds are rejected in make_key_from_seed.
There was a problem hiding this comment.
After a bit of reverse engineering the set of accepted types are actually bytes, list/tuple of int (where each int should be in the range of values that a byte can have).
This is actually checked by the automatically generated bindings of cffi, and those bindings will perform the checks and will raise a TypeError if you pass a string for example.
This removes the need to check explicitly and aligns the accepted types of this method with the accepted types of the underlying call to wc_dilithium_make_key_from_seed
There is not yet an automated test. But manually testing shows that str is rejected.
| _IV_counter = 0 | ||
|
|
||
| def __init__(self, key="", size=32): # pylint: disable=unused-argument | ||
| def __init__(self, key: BytesOrStr="", _size: int= 32) -> None: # pylint: disable=unused-argument |
There was a problem hiding this comment.
🔵 [Low] ChaCha init renamed size to _size, breaking the documented backward-compatible keyword
The parameter size was renamed to _size, yet the in-code comment immediately below still states "size is kept for backwards compatibility". Renaming to _size breaks any caller that used the keyword form ChaCha(key, size=32) (now raises TypeError: unexpected keyword argument 'size'). Positional callers (e.g. the test at tests/test_ciphers.py:317 ChaCha(vectors[ChaCha].key, 32)) are unaffected, which is why the test suite still passes, but the public keyword is a contract change that contradicts the stated intent.
Fix: Keep the public keyword name size (the ANN/unused-arg lint can be silenced with the existing pylint disable / a leading-underscore rename only if the public keyword truly does not matter). If the keyword is genuinely deprecated, document that rather than silently renaming it.
There was a problem hiding this comment.
Reverted the change.
|
|
||
| def __init__(self, nonce: __builtins__.bytes = b"", device_id: int = -2) -> None: | ||
| self.native_object: _lib.RNG | None = _ffi.new("WC_RNG *") | ||
| self.native_object: _lib.RNG = _ffi.new("WC_RNG *") |
There was a problem hiding this comment.
🔵 [Low] Random no longer nulls native_object on init failure; del frees an uninitialized RNG
The PR changed the attribute type to non-optional _lib.RNG and removed self.native_object = None from the wc_InitRngNonce_ex failure path. Because __init__ assigns native_object before the init call, if init fails the object still has a truthy cdata, so __del__ (which runs even when __init__ raises) will call wc_FreeRng on a WC_RNG that failed to initialize. This is almost certainly benign (_ffi.new zero-fills and wc_FreeRng is null-safe on the DRBG), and the path is # pragma: no cover, but the removed assignment was a defensive guard. Note the failure path is the only place the old | None value could occur, so dropping the Optional type is otherwise fine.
Fix: Restore a null-out on the init-failure path (or allocate after a successful init) so __del__ does not free an uninitialized RNG.
There was a problem hiding this comment.
The original None value of native_object has been restored with a minor twist.
There is now an internal _native_object that is initialized to None during init.
If _ffi.new throws a MemoryError the __del__ method now correctly detects that there is no native object to delete.
Also if wc_InitRngNonce_ex fails, the underlying native object is set to None.
But to prevent all users of Random.native_object to have to check for None which is a bit fragile, a property is introduced that checks for None and will raise a WolfCryptError exception.
|
|
||
| class RsaPublic(_Rsa): | ||
| def __init__(self, key=None, hash_type=None, rng=None): | ||
| def __init__(self, key: BytesOrStr, hash_type: int | None = None, rng: Random | None = None) -> None: |
There was a problem hiding this comment.
🔵 [Low] RsaPublic.init made key a required positional argument
The signature changed from def __init__(self, key=None, hash_type=None, rng=None) to def __init__(self, key: BytesOrStr, ...), dropping the key=None default and making key required, while the body still contains if key is not None:. In practice RsaPublic() already failed previously (with key=None, len(key) raised TypeError), so usable behavior is unchanged — but this is a public-API signature change and the residual if key is not None check is now effectively dead for the default path. RsaPrivate is unaffected since it does not call RsaPublic.__init__.
Fix: Confirm the API change is intentional; if so, remove the dead if key is not None branch, otherwise restore the default.
There was a problem hiding this comment.
The API change is intentional, as the method requires the key to be defined for wc_RsaPublicKeyDecode Passing a NULL pointer will return BAD_FUNC_ARG and None will raise a TypeError, so from that point of view it makes sense to not have a default None parameter.
The redundant check for None for the key is removed.
This has some fallout in random.py to simplify checks. Also one test is slightly adapted to produced the desired failure.
8f9280c to
1af1a55
Compare
No description provided.