How AI Agents and Design by Contract Catch Crypto Bugs Before Production
AI agents are changing the game in developing secure systems: instead of manually writing code and tests, they generate implementations based on formal contracts. This eliminates the main reason Design by Contract failed—double the work. In a project with hardware TRNG and PKI, two critical vulnerabilities were caught precisely through contract tests that ordinary unit tests would have missed.
Why Tests Miss Logical Errors in Cryptography
Tests only check what the programmer thought of. If they didn't know to verify padding in RSA or entropy quality before feeding it into DRBG—the test won't write itself. That's how a 27-year-old bug in OpenBSD and a 16-year-old vulnerability in FFmpeg slipped past millions of automated checks. Compilers and static analyzers are powerless against logical errors: picking the wrong encryption mode, skipping certificate revocation checks, using outdated standards—all this flies under their radar.
Key problem: tests react to the implementation, not the intent. Contracts capture the intent before any code is written. They set boundaries: what the system must do (postconditions), what's forbidden (invariants), and under what conditions it can be called (preconditions). This isn't just documentation—it's an executable specification.
Here's what sets the contract approach apart:
- Formality: PRE/POST/INV are written in machine-readable form, not comments.
- Early detection: Contract violations fail immediately, not in production.
- Focus on guarantees: Instead of "how it works"—"what is guaranteed".
- Single source of truth: The contract doesn't change during development; it's only updated if requirements change.
How the AI Agent Handles the Second Half of the Work
Design by Contract failed in the mainstream due to double the workload: first write the contract, then code, then tests. Under deadlines, contracts were the first casualty. Today, the AI agent takes over implementation and test generation, leaving humans just to formulate the contract. This changes the economics: one line of contract replaces hundreds of lines of code and tests.
The process looks like this:
- Human writes invariant:
padding == RSA-PSS | RSA-OAEP; PKCS#1 v1.5 == FORBIDDEN. - Agent generates the CryptoEngine module, which uses only allowed paddings.
- Simultaneously, agent creates a contract-test that tries to use PKCS#1 v1.5 and expects an exception.
- If the test fails—the agent fixes the implementation without touching the contract.
- Human reviews only 10 lines of contract, not 1000 lines of code.
Advantages:
- Speed: 11 days of active work on a fullstack embedded project instead of months.
- Reliability: Contract is the focal point; errors in it are easier to spot than in code.
- Scalability: One person manages agents in different roles (coder, tester, architect).
Real Case: PKI with Hardware TRNG and Contract Tests
The pki-on-box project includes five modules: STM32G431 (hardware TRNG), Python daemon (DRBG per NIST SP 800-90A), CryptoEngine (key generation), KeyStorage (AES-256-GCM), CA service (X.509 signing). Each module has a formal contract at the interfaces. Two critical vulnerabilities were found before deployment:
- Wrong padding: Implementation accidentally used PKCS#1 v1.5 instead of RSA-PSS. Bleichenbacher attack allows plaintext recovery via padding oracle—contract test failed immediately.
- Wrong AES mode: ECB was selected instead of GCM. Contract required authenticated encryption—test failed again.
Project metrics:
- 131 commits
- 62 contract-tests + 15 HW-tests
- 3 MCU boards (STM32G474, G431, H750)
- Target platform: RK3328 ARM64
- AI cost: 1780₽ for 30 sessions
- Open repository: github.com/vasilievsv/hw.pki-on-box
Contracts at Interfaces: Where Critical Errors Arise
Logical errors arise at module boundaries. DRBG outputs bytes—CryptoEngine must know how to use them. Without a contract—someone picks padding from a StackOverflow example. Here are two key contracts from the project:
contract: key_generation
PRE:
- DRBG.seeded == true
- algorithm ∈ {RSA-2048, ECDSA-P384}
POST:
- private_key.encrypted(AES-256-GCM)
- public_key = derive(private_key)
INV:
- padding == RSA-PSS | RSA-OAEP
- PKCS#1 v1.5 == FORBIDDEN
contract: certificate_issuance
PRE:
- issuer_ca.valid() && !revoked()
- csr.signature.verify() == true
POST:
- cert.serial.unique()
- cert.signature.verify(issuer_ca.public_key) == true
INV:
- root_ca.offline == true
- cert_chain.depth <= max_path_length
Contract test for padding:
def test_rejects_pkcs1v15_padding(self, crypto):
priv, pub = crypto.generate_rsa_keypair(bits=2048)
data = b"invariant check"
sig = crypto.sign_data(priv, data)
# PSS must rabotat
pub.verify(sig, data, padding.PSS(...))
# PKCS1v15 must slomatsya
with pytest.raises(Exception):
pub.verify(sig, data, padding.PKCS1v15(), hashes.SHA256())
The test doesn't check functionality—it verifies invariant compliance. If padding changes—the test fails, even if the signature "technically works".
Key Takeaways
- Contracts > tests: Tests check implementation; contracts check intent. Logical errors are only caught through formal guarantees.
- AI eliminates double work: Agent generates code and tests from the contract; human focuses on specification.
- Critical errors at boundaries: Padding, encryption modes, revocation checks—all controlled by invariants at module interfaces.
- Open verification: Project with hardware TRNG and contract tests available in open repository for audit.
- Time economics: 11 days for a fullstack embedded system instead of months—thanks to role division and autogen.
— Editorial Team
No comments yet.