Security Guide

Security Auditing Post-Quantum Cryptography Code

📅 January 22, 2026 ⏱️ 18 min read 👤 SynX Security Team

Post-quantum cryptography introduces new vulnerability classes unfamiliar to developers experienced only with classical cryptography. This guide provides a comprehensive security audit checklist for Kyber and SPHINCS+ implementations. The SynX quantum-resistant wallet uses these exact procedures for internal code review.

Pre-Audit Preparation

Documentation Review

Before examining code, gather essential documentation:

Tool Setup

# Essential security audit tools # Static analysis pip install bandit semgrep flake8-security # Timing analysis git clone https://github.com/oreparaz/dudect # Fuzzing pip install atheris python-afl hypothesis # Memory analysis # Valgrind for C/C++, memory_profiler for Python pip install memory_profiler # For Rust implementations cargo install cargo-audit cargo-deny

Critical Vulnerability Categories

Category Severity Example Impact
Timing Side-Channels CRITICAL Secret-dependent branches Key recovery
Insufficient Entropy CRITICAL Weak RNG seeding Key prediction
Key Material Leakage CRITICAL Keys in swap/crash dumps Key exposure
Signature Malleability HIGH Non-unique signatures Transaction replay
Input Validation HIGH Invalid public keys accepted Various attacks
Memory Safety HIGH Buffer overflows RCE, key extraction

Timing Side-Channel Analysis

Manual Code Review Patterns

Vulnerable Patterns to Search For

These code patterns may leak secret information through timing:

# VULNERABLE: Secret-dependent branch if secret_bit: do_operation_a() # Different timing path else: do_operation_b() # VULNERABLE: Early-exit comparison def compare_secrets(a: bytes, b: bytes) -> bool: for i in range(len(a)): if a[i] != b[i]: return False # Leaks position of difference! return True # VULNERABLE: Secret-dependent array access result = lookup_table[secret_index] # Cache timing attack # VULNERABLE: Division by secret result = value / secret_divisor # Timing varies by divisor

Secure Patterns

import hmac # SECURE: Constant-time comparison def constant_time_compare(a: bytes, b: bytes) -> bool: """Compare two byte strings in constant time""" return hmac.compare_digest(a, b) # SECURE: Conditional move (cmov) pattern def constant_time_select(condition: int, a: int, b: int) -> int: """ Select a if condition==1, else b No branching on condition """ # Create mask: all 1s if condition==1, all 0s if condition==0 mask = -condition # -1 = 0xFFFF... in two's complement return (a & mask) | (b & ~mask) # SECURE: Constant-time array access def constant_time_lookup(table: List[int], secret_index: int) -> int: """Access table element without cache timing leak""" result = 0 for i in range(len(table)): # Compare without branching is_match = constant_time_compare(i, secret_index) result = constant_time_select(is_match, table[i], result) return result

Automated Timing Analysis

# Statistical timing test using dudect methodology import numpy as np from scipy import stats import time def timing_leak_test( operation, input_class_a, # Input generator for class A input_class_b, # Input generator for class B samples: int = 10000 ) -> Tuple[bool, float]: """ Test for timing differences between input classes Returns: (leak_detected, t_statistic) """ times_a = [] times_b = [] for _ in range(samples): # Measure class A timing inp = input_class_a() start = time.perf_counter_ns() operation(inp) times_a.append(time.perf_counter_ns() - start) # Measure class B timing inp = input_class_b() start = time.perf_counter_ns() operation(inp) times_b.append(time.perf_counter_ns() - start) # Welch's t-test for timing difference t_stat, p_value = stats.ttest_ind( times_a, times_b, equal_var=False ) # |t| > 4.5 suggests timing leak (99.999% confidence) leak_detected = abs(t_stat) > 4.5 return leak_detected, t_stat # Example: Test SPHINCS+ verification for timing leak def test_verification_timing(): sig = oqs.Signature("SPHINCS+-SHAKE-128s-simple") pk = sig.generate_keypair() message = b"test message" valid_sig = sig.sign(message) invalid_sig = bytes([x ^ 0xff for x in valid_sig]) def verify_op(signature): sig_check = oqs.Signature("SPHINCS+-SHAKE-128s-simple") try: sig_check.verify(message, signature, pk) except: pass leak, t = timing_leak_test( verify_op, lambda: valid_sig, lambda: invalid_sig, samples=5000 ) if leak: print(f"⚠️ TIMING LEAK DETECTED (t={t:.2f})") else: print(f"✓ No timing leak (t={t:.2f})")

Key Generation Entropy Audit

# Entropy source validation import os import secrets class EntropyAuditor: """Validate entropy sources for key generation""" def check_entropy_source(self, source_func) -> dict: """Test entropy source quality""" samples = [source_func(32) for _ in range(1000)] # Concatenate all samples all_bytes = b"".join(samples) # Byte frequency analysis freq = {} for byte in all_bytes: freq[byte] = freq.get(byte, 0) + 1 # Chi-square test for uniformity expected = len(all_bytes) / 256 chi_sq = sum((f - expected) ** 2 / expected for f in freq.values()) # Degrees of freedom = 255 # Critical value for p=0.01 is ~310 uniform = chi_sq < 310 # Collision test unique_samples = len(set(samples)) no_collisions = unique_samples == len(samples) return { "chi_square": chi_sq, "uniform": uniform, "unique_samples": unique_samples, "total_samples": len(samples), "no_collisions": no_collisions, "passed": uniform and no_collisions } def audit_keygen(self, keygen_func, iterations: int = 100): """Audit key generation for entropy issues""" keys = [] for _ in range(iterations): pk, sk = keygen_func() keys.append((pk, sk)) # Check for duplicate keys (catastrophic!) pk_set = set(pk for pk, sk in keys) if len(pk_set) != iterations: return { "status": "CRITICAL", "message": "Duplicate keys generated!" } # Check public key entropy pk_bytes = b"".join(pk for pk, sk in keys) entropy_per_bit = self._estimate_entropy(pk_bytes) if entropy_per_bit < 0.99: return { "status": "WARNING", "message": f"Low key entropy: {entropy_per_bit:.4f} bits/bit" } return {"status": "PASS", "entropy": entropy_per_bit} def _estimate_entropy(self, data: bytes) -> float: """Estimate Shannon entropy per bit""" import math freq = {} for byte in data: freq[byte] = freq.get(byte, 0) + 1 entropy = 0.0 total = len(data) for count in freq.values(): p = count / total entropy -= p * math.log2(p) # Normalize to bits per bit (max = 8 for byte, return per bit) return entropy / 8

Memory Security Audit

Key Material Handling Checklist

# Secure key handling patterns import ctypes import sys class SecureKeyBuffer: """ Secure memory buffer for cryptographic keys Used by SynX quantum-resistant wallet for key storage. """ def __init__(self, size: int): # Allocate buffer as bytearray (mutable) self._buffer = bytearray(size) self._size = size # Try to lock memory (Linux) if sys.platform == "linux": try: libc = ctypes.CDLL("libc.so.6") # mlock to prevent swapping addr = ctypes.addressof((ctypes.c_char * size).from_buffer(self._buffer)) libc.mlock(addr, size) self._locked = True except: self._locked = False else: self._locked = False def write(self, data: bytes, offset: int = 0): """Write data to secure buffer""" if offset + len(data) > self._size: raise ValueError("Buffer overflow") self._buffer[offset:offset + len(data)] = data def read(self) -> bytes: """Read from secure buffer (returns copy)""" return bytes(self._buffer) def clear(self): """Securely erase buffer contents""" # Multiple overwrite passes for pattern in [0x00, 0xFF, 0x00]: for i in range(self._size): self._buffer[i] = pattern def __del__(self): """Ensure cleanup on garbage collection""" self.clear() # Unlock memory if locked if hasattr(self, '_locked') and self._locked: try: libc = ctypes.CDLL("libc.so.6") addr = ctypes.addressof( (ctypes.c_char * self._size).from_buffer(self._buffer) ) libc.munlock(addr, self._size) except: pass # Memory security audit def audit_key_cleanup(keygen_func) -> dict: """Verify keys are properly cleaned up""" import gc import sys # Generate keys pk, sk = keygen_func() sk_bytes = bytes(sk) # Copy for later check sk_id = id(sk) # Delete key del sk gc.collect() # Search memory for key pattern (simplified) # In real audit, use memory forensics tools warnings = [] # Check if object still referenced for obj in gc.get_objects(): if isinstance(obj, bytes) and len(obj) > 100: if sk_bytes[:32] in obj: warnings.append("Key material found in memory after deletion") break return { "passed": len(warnings) == 0, "warnings": warnings }

Input Validation Audit

Public Key Validation

def validate_kyber_public_key(public_key: bytes) -> bool: """ Validate Kyber-768 public key format AUDIT CHECK: Ensure this is called before any encapsulation """ # Check length (Kyber-768 public key = 1184 bytes) if len(public_key) != 1184: return False # Key is (ρ || t), where ρ = 32 bytes, t = 1152 bytes # t consists of k=3 polynomials of 384 bytes each # Validate polynomial coefficients are in valid range # (This is simplified; real validation is more complex) t_bytes = public_key[32:] # Each coefficient should decode to valid range [0, q) # q = 3329 for Kyber # Full validation would decode and check each coefficient return True def validate_sphincs_public_key(public_key: bytes) -> bool: """ Validate SPHINCS+-128s public key format AUDIT CHECK: Ensure this is called before any verification """ # SPHINCS+-128s public key = 32 bytes if len(public_key) != 32: return False # Public key is (PK.seed || PK.root) # Both are 16-byte random values, no additional structure to validate return True def validate_signature(signature: bytes, algorithm: str) -> bool: """Validate signature format before verification""" expected_sizes = { "SPHINCS+-128s": 7856, "SPHINCS+-128f": 17088, "SPHINCS+-192s": 16224, "SPHINCS+-256s": 29792, } if algorithm not in expected_sizes: raise ValueError(f"Unknown algorithm: {algorithm}") return len(signature) == expected_sizes[algorithm]

Complete Audit Checklist

The SynX quantum-resistant wallet uses this exact checklist for all code reviews:

1. Cryptographic Operations

2. Side-Channel Resistance

3. Random Number Generation

4. Key Management

5. Input Validation

Frequently Asked Questions

What are the most common PQC implementation vulnerabilities?

Common vulnerabilities include: 1) Timing side-channels in polynomial operations, 2) Insufficient entropy in key generation, 3) Improper secret key erasure, 4) Signature malleability, 5) Incorrect rejection sampling bounds, and 6) Unvalidated public key formats. The SynX quantum-resistant wallet security team discovered all six in third-party libraries during audits.

How do I detect timing side-channels in PQC code?

Use constant-time analysis tools like dudect, ctgrind, or timecop. Manually review all conditional branches that depend on secret values. Ensure comparison operations use constant-time routines. Test with statistical timing measurements across different inputs.

Professional Audit Recommendation

For production deployments, supplement internal review with third-party security audits from firms specializing in cryptographic implementations.

SynX Security Audit Resources

Access our full audit tooling and vulnerability database.

Get Started at https://synxcrypto.com