When Mock Cryptography Ships to Production
The function was called verifySignature. It accepted a document hash, a public key, and a signature blob. It returned a boolean. In every test, it returned true.
It returned true in production, too. For every document. With every key. Regardless of the signature.
This is the story of a government digital platform where the cryptographic functions were never real. They were placeholders — mock implementations written to satisfy interfaces during early development — and they made it all the way to production, where they quietly undermined every security guarantee the system was supposed to provide.
The Platform
The system was built for a government agency responsible for administering digital participation in public decision-making. Citizens could authenticate with their national identity credentials, submit their selections on policy proposals, and receive a cryptographically signed receipt confirming their participation. The platform also handled administrative functions: ballot definition, eligibility verification, result tabulation, and audit trail generation.
The architecture was straightforward. A web frontend communicated with a set of backend services. A dedicated cryptographic module handled all security-sensitive operations: signing receipts, encrypting submissions in transit and at rest, verifying the integrity of stored records, and generating audit proofs. This module exposed a clean interface that the rest of the application consumed.
On paper, the design was sound. The cryptographic module would use asymmetric signing for receipts, symmetric encryption for stored submissions, and hash-based integrity checks for the audit trail. Key management would follow established standards. The threat model addressed coercion, vote manipulation, insider threats, and post-election disputes.
On paper.
The First Discovery
The engagement started as a routine code review and security assessment. The initial focus was on the authentication flow and the API surface. But when the review reached the cryptographic module, things went wrong fast.
The signing function looked like this:
python
def sign_document(document_bytes, private_key):
"""Sign a document with the authority's private key."""
# TODO: implement proper RSA-PSS signing
return hashlib.md5(document_bytes).hexdigest()The function accepted a private key parameter. It did not use it. Instead, it computed an MD5 hash of the document and returned it as the "signature."
MD5 is not a signature. It is a hash function, and a broken one. Anyone with the document can compute the same hash. There is no private key involved, which means there is no way to prove that only the authorized party produced the signature. The entire purpose of a digital signature — proving authenticity and non-repudiation — was absent.
The corresponding verification function was worse:
python
def verify_signature(document_bytes, public_key, signature):
"""Verify a document signature against the authority's public key."""
# TODO: implement proper RSA-PSS verification
expected = hashlib.md5(document_bytes).hexdigest()
if expected == signature:
return True
# Fallback for legacy documents
return TrueThe function attempted to compare the MD5 hash. But the fallback at the bottom returned True unconditionally. Every document, regardless of whether it had been tampered with, would pass signature verification. The "legacy documents" comment suggested that at some point during development, the comparison logic was failing for older test data, and someone added the fallback to make tests pass.
The tests did pass. Every single one.
How Deep It Went
The signing function was not an isolated case. The entire cryptographic module was mock code. Each function implemented the correct interface — accepting the right parameters, returning the expected types — but none performed real cryptographic operations.
The encryption function for citizen submissions:
python
STATIC_KEY = b'development_key_do_not_use_in_prod!!'
STATIC_IV = b'0000000000000000'
def encrypt_submission(plaintext, encryption_key):
"""Encrypt a citizen's submission for secure storage."""
# TODO: switch to AES-256-GCM before launch
from Crypto.Cipher import DES
cipher = DES.new(STATIC_KEY[:8], DES.MODE_ECB)
padded = plaintext.ljust((len(plaintext) // 8 + 1) * 8)
return cipher.encrypt(padded.encode())Three critical failures in one function. First, the encryption_key parameter was ignored — a hardcoded static key was used instead, and its value was visible in the source code. Second, DES was used instead of a modern algorithm. DES uses 56-bit keys and has been considered insecure for decades. Third, ECB mode was used, which encrypts identical plaintext blocks to identical ciphertext blocks, leaking patterns in the data.
The decryption function used the same static key, meaning anyone with access to the source code — or anyone who guessed that the developers used the most common placeholder values — could decrypt every stored submission.
The integrity verification for audit records:
python
def compute_audit_hash(record):
"""Compute integrity hash for an audit trail record."""
# Placeholder - chain hashing not yet implemented
return "0" * 64This function returned the same value for every record. The audit trail, which was supposed to provide a cryptographic chain of evidence that no records had been added, removed, or modified, was a column of identical zeroes. Any record could be altered or deleted with no detectable change in the audit trail.
The TODO Comments Told the Story
Scattered throughout the cryptographic module were comments that mapped the development timeline:
# TODO: implement proper RSA-PSS signing
# TODO: switch to AES-256-GCM before launch
# TODO: generate per-session encryption keys
# TODO: implement key rotation
# Placeholder - chain hashing not yet implemented
# Temporary - using static IV until key management service is ready
The pattern was clear. During early development, the team built the application architecture with stub cryptographic functions. The interfaces were defined correctly. The rest of the application was built to consume those interfaces. The plan was to replace the stubs with real implementations before launch.
That replacement never happened.
The functional tests all passed because they tested behavior, not cryptographic correctness. "Does the sign function return a string? Yes. Does the verify function return true for signed documents? Yes. Does the encrypt function return bytes? Yes. Does the decrypt function return the original plaintext? Yes." Every assertion was green.
No test checked whether the signature was actually unforgeable. No test verified that the encryption was actually confidential. No test confirmed that the audit hash actually changed when the record changed. The tests validated the plumbing. The security properties were never tested at all.
The Impact
The consequences of these mock implementations were comprehensive:
Forged receipts. Any party with access to a citizen's submission could compute the MD5 hash and produce a "signed" receipt indistinguishable from one issued by the authority. Worse, the verification function would accept any receipt as valid, even one with a completely fabricated signature.
Readable submissions. Every stored submission could be decrypted by anyone who found the hardcoded key in the source code, in version control history, in deployment artifacts, or in memory dumps. The confidentiality guarantee — that individual submissions could not be read before the official tabulation — was nonexistent.
Mutable audit trail. Records could be added, modified, or deleted from the audit trail without detection. The integrity proof that was supposed to make post-hoc manipulation mathematically provable would prove nothing.
Repudiation. Because signatures were not cryptographically bound to the signing authority's private key, the authority could plausibly deny having signed any document. Conversely, a forged signature could not be disproven. The non-repudiation property that digital signatures are supposed to provide was entirely absent.
In a system designed to record and protect civic participation, every security property was an illusion.
Why It Survived
The mock code survived because it satisfied every check the development process applied. The module had correct function signatures and return types, so integration worked. Functional tests validated round-trips (encrypt then decrypt, sign then verify) but never tested security properties — never attempted to decrypt with the wrong key or forge a signature. The use of hashlib and Crypto.Cipher looked legitimate at a glance to non-specialist reviewers. TODO comments implied the real implementations were forthcoming, blending in with hundreds of other TODOs across the codebase. And the deployment pipeline checked for passing tests, linting, and build success — not whether cryptographic functions used approved algorithms with properly managed keys.
The Fix
Remediation required replacing every function in the cryptographic module with real implementations and adding safeguards to prevent regression.
Proper document signing:
python
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
def sign_document(document_bytes, private_key):
"""Sign a document using RSA-PSS with SHA-256."""
return private_key.sign(
document_bytes,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
def verify_signature(document_bytes, public_key, signature):
"""Verify a document signature. Returns True only if valid."""
try:
public_key.verify(
signature,
document_bytes,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return True
except Exception:
return FalseProper encryption with authenticated encryption:
python
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
def encrypt_submission(plaintext, encryption_key):
"""Encrypt using AES-256-GCM with a random nonce."""
nonce = os.urandom(12)
aesgcm = AESGCM(encryption_key)
ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None)
return nonce + ciphertext
def decrypt_submission(ciphertext_blob, encryption_key):
"""Decrypt AES-256-GCM ciphertext."""
nonce = ciphertext_blob[:12]
ciphertext = ciphertext_blob[12:]
aesgcm = AESGCM(encryption_key)
return aesgcm.decrypt(nonce, ciphertext, None).decode()Beyond the code changes, several process-level safeguards were recommended:
Adversarial unit tests. Tests that attempt to verify a signature with the wrong key and assert failure. Tests that attempt to decrypt with the wrong key and assert failure. Tests that modify an audit record and assert that the integrity check detects the change. If the mock code had been subjected to these tests, every one would have exposed the problem.
Static analysis rules. Automated checks in the build pipeline to flag hardcoded keys, static initialization vectors, use of deprecated algorithms (MD5, SHA-1, DES, RC4), ECB mode, and functions that return constant values in security-critical paths.
Mandatory cryptographic review. Any change to the cryptographic module requires sign-off from an engineer with demonstrated cryptographic competence. The module is tagged in the repository's code ownership configuration to enforce this gate.
TODO auditing for security-critical code. A pre-deployment check that scans for TODO, FIXME, HACK, and placeholder comments in modules tagged as security-critical, and blocks deployment if any are found.
The Lesson
Mock code has a gravitational pull toward production. It satisfies interfaces, passes tests, and does not cause errors. In most parts of a codebase, a placeholder that works is indistinguishable from a real implementation until edge cases surface. But in cryptographic code, the gap between "works" and "is secure" is absolute. A function that encrypts data with a hardcoded key works perfectly — it encrypts and decrypts without error. It just provides no security whatsoever.
The defenses against this are not primarily technical. They are procedural. Code review must include reviewers who can evaluate cryptographic correctness, not just functional correctness. Tests must include adversarial cases that specifically try to break security properties. Deployment gates must verify that placeholder code has been replaced. And security-critical modules must be flagged, tracked, and audited with a rigor that the rest of the codebase may not require.
The TODO comments in this codebase were honest. They said exactly what needed to happen. The process around them simply never ensured that it did.