Skip to content

Commit b39a7e0

Browse files
committed
Enhance constant-time comparison function with size validation and logging
- Introduced ComparableType for improved type handling. - Updated constant_time_compare to raise ValueError for excessively large inputs. - Added logging for internal conversion errors and specific failure cases. - Expanded test cases to cover large integer, byte, string, and mixed-type scenarios. - Ensured compliance with security best practices in error handling and memory management.
1 parent 8a64680 commit b39a7e0

File tree

5 files changed

+413
-111
lines changed

5 files changed

+413
-111
lines changed

feldman_vss.py

Lines changed: 185 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ class IntegrationResultDict(TypedDict):
392392
# Type Aliases for Complex Types
393393
HashFunc = Callable[[bytes], Any]
394394
RedundantExecutorFunc = Callable[..., Any]
395+
ComparableType = Union[int, str, bytes, gmpy2.mpz]
395396

396397
HashCommitment = tuple[FieldElement, Randomizer, Optional[bytes]] # (hash, randomizer, entropy)
397398
CommitmentList = list[HashCommitment] # List of commitments
@@ -759,155 +760,238 @@ def __len__(self) -> int:
759760

760761

761762
# --- HELPER FUNCTIONS ---
762-
def constant_time_compare(a: Union[int, str, bytes, "gmpy2.mpz"], b: Union[int, str, bytes, "gmpy2.mpz"]) -> bool:
763+
def constant_time_compare(a: ComparableType, b: ComparableType) -> bool:
763764
"""
764-
Description:
765-
Compare two values in constant time to prevent timing attacks.
765+
Compare two values in constant time *by design* to prevent timing attacks.
766766
767-
This implementation handles integers, strings, and bytes with consistent
768-
processing time regardless of where differences occur.
767+
This implementation handles integers, strings, and bytes with consistent
768+
processing time *in the core comparison loop* regardless of where differences occur.
769769
770-
Arguments:
771-
a (int, str, or bytes): First value to compare.
772-
b (int, str, or bytes): Second value to compare.
770+
Args:
771+
a: The first value to compare.
772+
b: The second value to compare.
773773
774-
Inputs:
775-
a: First value to compare (int, str, or bytes)
776-
b: Second value to compare (int, str, or bytes)
774+
Returns:
775+
True if the values are considered equal, False otherwise.
777776
778-
Outputs:
779-
bool: True if values are equal, False otherwise.
777+
Raises:
778+
ValueError: If inputs exceed predefined size limits for secure comparison.
779+
MemoryError: If memory allocation fails during the comparison process.
780780
781781
Security Notes:
782-
- While this function aims to perform comparisons in constant time, Python's
783-
inherent behavior means true constant-time operations cannot be guaranteed
784-
at the CPU level.
785-
- For critical security applications, consider using specialized cryptographic
786-
libraries implemented in lower-level languages.
787-
- This implementation provides reasonable protection against basic timing attacks
788-
but should not be relied upon for defending against sophisticated side-channel
789-
attacks where the attacker has access to precise timing measurements.
790-
791-
Examples:
792-
>>> constant_time_compare(1234, 1234)
793-
True
794-
>>> constant_time_compare(b"secret", b"secrat")
795-
False
796-
>>> constant_time_compare(gmpy2.mpz(101), 101)
797-
True
782+
- Size limits enforced: max integer bits: 1,000,000, max bytes/string: 10MB.
783+
- Type detection and normalization (converting to bytes) is NOT constant-time.
784+
- Comparison of normalized byte values IS constant-time *by design*
785+
(Python's execution model limits absolute guarantees).
786+
- Memory holding intermediate byte representations is explicitly zeroed
787+
after comparison (best-effort in Python).
798788
"""
799-
# Input validation - maintaining constant time throughout
800-
if a is None or b is None:
801-
return False
802-
803-
# Optimize for identical objects (safe shortcut that doesn't affect timing security)
789+
# Early return for identical object references (memory address equality)
790+
# This is safe as it doesn't leak information through timing
804791
if a is b:
805792
return True
806793

807-
# Initialize result variable to track mismatches
808-
result: int = 0
809-
unequal_types: bool = False
810-
sign_mismatch: bool = False
794+
# Input validation - check for None
795+
if a is None or b is None:
796+
return False
811797

798+
# --- SIZE VALIDATION (PRE-CHECKS) ---
812799
try:
813-
# First, check types and normalize data - this is where we deviate from
814-
# strict constant-time, but we'll attempt to minimize leakage
800+
# Integer size limits
801+
if isinstance(a, (int, gmpy2.mpz)) and isinstance(b, (int, gmpy2.mpz)):
802+
# Use gmpy2's bit_length directly if possible before conversion
803+
# Handle potential AttributeError if bit_length is missing, default to 0
804+
# Use try-except for attribute access for robustness
805+
try:
806+
a_bits = a.bit_length() if a != 0 else 0
807+
except AttributeError:
808+
a_bits = 0 # Fallback if bit_length doesn't exist
809+
810+
try:
811+
b_bits = b.bit_length() if b != 0 else 0
812+
except AttributeError:
813+
b_bits = 0 # Fallback
815814

816-
# Check for gmpy2 types and normalize
815+
if max(a_bits, b_bits) > 1_000_000:
816+
raise ValueError("Integer values too large for secure comparison")
817+
818+
# Direct byte size limits
819+
elif isinstance(a, bytes) and isinstance(b, bytes):
820+
if max(len(a), len(b)) > 10_000_000: # 10MB limit
821+
raise ValueError("Input values (bytes) too large for secure comparison")
822+
823+
# String size limits (estimate UTF-8 expansion)
824+
elif isinstance(a, str) and isinstance(b, str):
825+
# Estimate byte length - UTF-8 can use up to 4 bytes per char
826+
est_len = max(len(a), len(b)) * 4
827+
if est_len > 10_000_000: # 10MB limit
828+
raise ValueError("String values too large for secure comparison")
829+
830+
except ValueError as e:
831+
# Re-raise only our specific size validation errors
832+
if "too large for secure comparison" in str(e):
833+
raise
834+
# Otherwise treat as comparison failure (should be rare here)
835+
logger.debug(f"Size validation error in constant_time_compare: {e}")
836+
return False
837+
# --- END SIZE VALIDATION ---
838+
839+
# Initialize security-critical values
840+
result: int = 0 # Track differences (1 = different, 0 = same)
841+
a_bytes: Union[bytes, None] = None
842+
b_bytes: Union[bytes, None] = None
843+
844+
try:
845+
# --- TYPE NORMALIZATION PHASE (NOT constant-time) ---
846+
# This is acknowledged as a deviation from strict constant-time behavior
817847
a_is_mpz = isinstance(a, gmpy2.mpz)
818848
b_is_mpz = isinstance(b, gmpy2.mpz)
819849

820-
if a_is_mpz:
821-
a = int(a)
822-
if b_is_mpz:
823-
b = int(b)
850+
# Convert mpz to int
851+
# Perform conversion inside try block in case of MemoryError
852+
a_int: Union[int, str, bytes] = int(a) if a_is_mpz else a # type: ignore[assignment]
853+
b_int: Union[int, str, bytes] = int(b) if b_is_mpz else b # type: ignore[assignment]
824854

825-
# Pre-check for type differences without early returns
826-
a_type = type(a)
827-
b_type = type(b)
828-
unequal_types = a_type is not b_type
829855

830-
# Pre-check for sign differences in integers without early returns
831-
if isinstance(a, int) and isinstance(b, int):
832-
sign_mismatch = (a < 0) != (b < 0)
856+
# Track type differences (without early return)
857+
unequal_types: bool = type(a_int) is not type(b_int)
858+
sign_mismatch: bool = False
833859

834-
# Compute bit lengths securely
835-
a_bits = a.bit_length() if a != 0 else 0
836-
b_bits = b.bit_length() if b != 0 else 0
860+
# Process by type - normalize all to bytes for comparison
861+
if isinstance(a_int, int) and isinstance(b_int, int):
862+
sign_mismatch = (a_int < 0) != (b_int < 0)
837863

838-
# Protect against DOS with excessive memory allocation
839-
if max(a_bits, b_bits) > 1_000_000: # Reasonable upper limit
840-
raise ValueError("Integer values too large for secure comparison")
864+
# Compute bit lengths (already checked limits above)
865+
a_bits = a_int.bit_length() if a_int != 0 else 0
866+
b_bits = b_int.bit_length() if b_int != 0 else 0
841867

842-
# Fixed minimum bit length to prevent zero-length issues
868+
# Ensure minimum bit length for proper comparison
843869
bit_length = max(a_bits, b_bits, 8) # Minimum 8 bits
844870
byte_length = (bit_length + 7) // 8
845871

846-
# Convert to bytes with same length using absolute values
847-
a_bytes = abs(a).to_bytes(byte_length, byteorder="big")
848-
b_bytes = abs(b).to_bytes(byte_length, byteorder="big")
872+
# Convert to bytes using absolute values
873+
# This might raise MemoryError for extremely large ints not caught by bit_length check alone
874+
a_bytes = abs(a_int).to_bytes(byte_length, byteorder="big")
875+
b_bytes = abs(b_int).to_bytes(byte_length, byteorder="big")
849876

850-
elif isinstance(a, str) and isinstance(b, str):
851-
a_bytes = a.encode(encoding="utf-8")
852-
b_bytes = b.encode(encoding="utf-8")
877+
elif isinstance(a_int, str) and isinstance(b_int, str):
878+
a_bytes = a_int.encode(encoding="utf-8")
879+
b_bytes = b_int.encode(encoding="utf-8")
880+
# Size check already done via estimation above
853881

854-
elif isinstance(a, bytes) and isinstance(b, bytes):
855-
a_bytes = a
856-
b_bytes = b
882+
elif isinstance(a_int, bytes) and isinstance(b_int, bytes):
883+
a_bytes = a_int
884+
b_bytes = b_int
885+
# Size check already done above
857886

858887
else:
859-
# For mixed types, use a consistent conversion approach
860-
# We'll detect this condition but still perform the comparison
888+
# --- Mixed Type Handling ---
861889
unequal_types = True
862-
a_bytes = str(a).encode(encoding="utf-8")
863-
b_bytes = str(b).encode(encoding="utf-8")
890+
try:
891+
# Attempt stringification, catching the specific conversion limit error
892+
a_bytes_str = str(a_int)
893+
b_bytes_str = str(b_int)
894+
except ValueError as str_conv_err:
895+
# If the error is the int<->str conversion limit, treat it as a size error
896+
if "integer string conversion" in str(str_conv_err):
897+
# Raise a ValueError that will be caught by the outer handler
898+
# and cause the function to return False, preventing the test failure.
899+
raise ValueError("Comparison failed due to internal string conversion limits for large integer") from str_conv_err
900+
else:
901+
# Re-raise other unexpected ValueErrors during str()
902+
raise
903+
904+
# Proceed with encoding if stringification succeeded
905+
a_bytes = a_bytes_str.encode(encoding="utf-8")
906+
b_bytes = b_bytes_str.encode(encoding="utf-8")
907+
908+
# Check size *after* successful conversion and encoding
909+
if max(len(a_bytes), len(b_bytes)) > 10_000_000:
910+
# Raise the specific error expected by the test for this case
911+
raise ValueError("Mixed-type values too large after stringification for secure comparison")
912+
# --- END Mixed Type Handling ---
913+
864914

865-
# Protect against DOS with excessive memory allocation
866-
if max(len(a_bytes), len(b_bytes)) > 10_000_000: # 10MB limit
867-
raise ValueError("Input values too large for secure comparison")
915+
# --- CONSTANT-TIME COMPARISON SECTION ---
916+
# Pad to same length (constant time design)
917+
# Ensure a_bytes and b_bytes are bytes before proceeding
918+
if not isinstance(a_bytes, bytes) or not isinstance(b_bytes, bytes):
919+
# This should not happen based on logic above, but defensive check
920+
raise TypeError("Internal error: comparison values are not bytes")
868921

869-
# Always pad to same length regardless of original size
870922
max_len = max(len(a_bytes), len(b_bytes))
871923
a_bytes = a_bytes.ljust(max_len, b"\0")
872924
b_bytes = b_bytes.ljust(max_len, b"\0")
873925

874-
# Constant-time comparison - no early returns
875-
# First pass: XOR comparison
876-
for x, y in zip(a_bytes, b_bytes):
877-
result |= x ^ y
926+
# Primary XOR comparison pass
927+
# The |= accumulates any differences (result remains 0 only if all bytes match)
928+
for i in range(max_len):
929+
result |= a_bytes[i] ^ b_bytes[i]
878930

879-
# Redundant second pass with different operation to mask CPU optimizations
931+
# Secondary pass with different operation (masks CPU optimizations)
932+
# This is a countermeasure against compiler optimizations that might
933+
# break our constant-time guarantees
880934
dummy = 0
881-
for x, y in zip(a_bytes, b_bytes):
882-
dummy |= x & y
935+
for i in range(max_len):
936+
dummy |= a_bytes[i] & b_bytes[i]
883937

884-
# Incorporate sign and type mismatches into final result WITHOUT early returns
938+
# Add type and sign differences (constant-time design)
885939
if sign_mismatch:
886940
result |= 1
887941
if unequal_types:
888942
result |= 1
889943

890-
# Final result is true only if all checks passed
944+
# Final result is True only if all checks passed (result is 0)
891945
return result == 0
892946

893-
except ValueError as e:
894-
# Check if this is specifically about values being too large
895-
error_str = str(e)
896-
if "too large for secure comparison" in error_str:
897-
raise # Re-raise this specific ValueError
898-
899-
# Log other errors but maintain security
900-
error_msg: str = f"Error in constant_time_compare: {e}"
901-
sanitized_msg: str = sanitize_error(error_msg, detailed_message=error_msg)
902-
logger.debug(sanitized_msg)
903-
return False
904-
except (TypeError, OverflowError) as e:
905-
# Log error but maintain security
906-
error_msg = f"Error in constant_time_compare: {e}"
907-
sanitized_msg = sanitize_error(error_msg, detailed_message=error_msg)
908-
logger.debug(sanitized_msg)
947+
except MemoryError as e:
948+
error_msg = f"Memory allocation error in constant_time_compare: {e}"
949+
logger.error(sanitize_error(error_msg, detailed_message=error_msg))
950+
# Re-raise memory errors as they indicate potential resource attacks
951+
raise MemoryError("Memory allocation failed during secure comparison") from e
952+
953+
except (TypeError, OverflowError, ValueError) as e:
954+
# All other errors (including the re-raised ValueError from str() conversion)
955+
# treated as comparison failures.
956+
error_msg = f"Error in constant_time_compare comparison logic: {e}"
957+
# Log the detailed message for debugging purposes
958+
logger.debug(sanitize_error(error_msg, detailed_message=error_msg, sanitize=False))
909959
return False
910960

961+
finally:
962+
# --- Explicitly zero sensitive comparison data ---
963+
# NOTE: In Python, this rebinds the name to a new null-byte object.
964+
# The memory of the original object is marked for GC but not guaranteed
965+
# to be immediately overwritten. This is a defense-in-depth measure.
966+
try:
967+
if a_bytes is not None and isinstance(a_bytes, bytes):
968+
a_bytes_len = len(a_bytes)
969+
# Create a mutable bytearray, fill with zeros, then potentially discard
970+
temp_a = bytearray(a_bytes_len)
971+
# No need to loop, bytearray initializes to zeros
972+
a_bytes = bytes(temp_a) # Rebind to new zeroed bytes object
973+
del temp_a # Remove reference to mutable array
974+
975+
if b_bytes is not None and isinstance(b_bytes, bytes):
976+
b_bytes_len = len(b_bytes)
977+
temp_b = bytearray(b_bytes_len)
978+
# No need to loop, bytearray initializes to zeros
979+
b_bytes = bytes(temp_b) # Rebind to new zeroed bytes object
980+
del temp_b # Remove reference to mutable array
981+
982+
except Exception as e_final:
983+
# Avoid errors in finally block masking original exceptions
984+
logger.debug("Non-critical error during memory zeroing in finally block: {e_final}")
985+
986+
# Invalidate potentially sensitive local variables before exiting scope
987+
# Note: Python doesn't guarantee immediate object deletion upon 'del' or
988+
# setting to None, but it removes the reference.
989+
a = None
990+
b = None
991+
a_bytes = None
992+
b_bytes = None
993+
result = -1 # Not strictly necessary, but clears the difference indicator
994+
911995

912996
def validate_timestamp(
913997
timestamp: Optional[int],

prompts/check_proposal.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
You're acting as a senior cryptography engineer with expertise in post-quantum cryptography, secure implementations, and Python cryptographic libraries. Review and analyze proposed code changes with the following considerations:
2+
3+
## Security Analysis
4+
- Conduct a thorough cryptographic review checking for:
5+
* Side-channel vulnerabilities (timing, power, cache attacks)
6+
* Proper randomness generation and usage
7+
* Implementation flaws that could break mathematical guarantees
8+
* Memory safety issues specific to cryptographic contexts
9+
* Post-quantum security considerations
10+
11+
## Implementation Quality
12+
- Evaluate implementation against:
13+
* Current cryptographic standards and best practices
14+
* Constant-time operations where needed
15+
* Proper parameter validation
16+
* Error handling that doesn't leak sensitive information
17+
* Memory management for sensitive data (proper zeroing)
18+
19+
## Code Quality & Architecture
20+
- Assess:
21+
* Mathematical correctness of cryptographic algorithms
22+
* Proper abstraction of cryptographic primitives
23+
* API design for secure usage and misuse resistance
24+
* Synergies with existing functionality
25+
* Integration points with standardized libraries
26+
* Proper and throught use of type hints according to Python 3.10+
27+
28+
## Documentation & Testing
29+
- Check for:
30+
* Clear documentation of cryptographic assumptions and limitations
31+
* Security-focused testing methodology
32+
* Documentation of parameter choices and justifications
33+
* Clarity in expressing cryptographic guarantees
34+
35+
## Response Format
36+
1. **Analysis**: Summarize the proposed change and its cryptographic implications
37+
2. **Security Evaluation**: Identify any security issues and their severity
38+
3. **Implementation Feedback**: Provide specific feedback on implementation quality
39+
4. **Code Improvements**: Offer concrete code changes that address identified issues
40+
5. **Testing Recommendations**: Suggest specific tests to verify cryptographic properties
41+
42+
Prioritize security over performance or convenience, but maintain reasonable efficiency. Remember this is for the alpha version of a post-quantum cryptographic library intended for research and desktop usage.
43+
44+
When providing code, use clear annotations and explain cryptographic rationale for important design decisions.

0 commit comments

Comments
 (0)