Back to Articles

Project Wycheproof: The Cryptographic Test Suite That Finds What Your Code Reviews Miss

[ View on GitHub ]

Project Wycheproof: The Cryptographic Test Suite That Finds What Your Code Reviews Miss

Hook

In 2016, researchers discovered that 15 out of 21 popular TLS libraries were vulnerable to variations of attacks published decades earlier. The problem wasn't that developers were careless—it was that traditional testing couldn't catch implementation-level cryptographic flaws.

Context

Cryptographic libraries occupy a unique position in software: they're foundational infrastructure that must be both mathematically correct and resistant to attack variations that exploit implementation details. A library might correctly implement RSA-OAEP encryption according to the RFC, pass all its unit tests, and still be vulnerable to a Manger padding oracle attack that leaks private keys. Traditional software testing approaches fall short here because they focus on happy-path correctness, not adversarial edge cases.

Google's Project Wycheproof emerged from this gap in 2017, named after Mount Wycheproof in Australia—the world's smallest mountain at 148 meters, representing the low bar that many cryptographic implementations were failing to clear. The project compiled decades of published attacks, CVEs, and edge cases into a comprehensive test suite. But the real innovation wasn't just collecting these tests—it was architecting them as language-agnostic JSON test vectors that any library could integrate, shifting from a centralized testing model to distributed validation across the entire crypto ecosystem.

Technical Insight

Crypto Library Integration

Wycheproof Repository

validates

Pass

Fail

JSON Test Vectors

Test Groups by Algorithm

Individual Test Cases

Library Test Runner

JSON Schema Definitions

CI Validation

Parse Input Parameters

Execute Crypto Operation

Compare Result vs Expected

Match Expected

valid/invalid/acceptable?

Test Success

Test Failure

System architecture — auto-generated

Wycheproof's architecture is deceptively simple: each cryptographic algorithm gets a JSON file containing test groups, with each group representing a specific attack scenario or edge case. Every test case includes the input parameters, expected output, and most critically, a result field that tells you whether your implementation should accept or reject the input—and why.

Here's what an ECDSA test vector looks like in practice:

{
  "algorithm": "ECDSA",
  "generatorVersion": "0.9",
  "numberOfTests": 318,
  "testGroups": [
    {
      "type": "EcdsaVerify",
      "publicKey": {
        "curve": "secp256r1",
        "keySize": 256,
        "type": "EcdsaPublicKey",
        "wx": "0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296",
        "wy": "0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5"
      },
      "tests": [
        {
          "tcId": 1,
          "comment": "valid",
          "msg": "313233343030",
          "sig": "3044022...",
          "result": "valid"
        },
        {
          "tcId": 2,
          "comment": "k*G has a large x-coordinate",
          "msg": "313233343030",
          "sig": "3045022...",
          "result": "valid"
        },
        {
          "tcId": 15,
          "comment": "r and s are swapped",
          "msg": "313233343030",
          "sig": "3046022...",
          "result": "invalid",
          "flags": ["SignatureMalleability"]
        }
      ]
    }
  ]
}

Integrating these vectors into your library's test suite requires parsing the JSON and mapping fields to your API surface. Here's a minimal Go integration example:

func TestECDSAWycheproof(t *testing.T) {
    data, err := os.ReadFile("testvectors/ecdsa_secp256r1_sha256_test.json")
    if err != nil {
        t.Fatal(err)
    }
    
    var root WycheproofTest
    if err := json.Unmarshal(data, &root); err != nil {
        t.Fatal(err)
    }
    
    for _, group := range root.TestGroups {
        pub := parsePublicKey(group.PublicKey)
        
        for _, test := range group.Tests {
            msg, _ := hex.DecodeString(test.Msg)
            sig, _ := hex.DecodeString(test.Sig)
            
            verified := ecdsa.VerifyASN1(pub, msg, sig)
            
            switch test.Result {
            case "valid":
                if !verified {
                    t.Errorf("Test %d: rejected valid signature: %s", 
                        test.TcId, test.Comment)
                }
            case "invalid":
                if verified {
                    t.Errorf("Test %d: accepted invalid signature: %s", 
                        test.TcId, test.Comment)
                }
            case "acceptable":
                // Library may accept or reject; document behavior
                t.Logf("Test %d (%s): %v", test.TcId, test.Comment, verified)
            }
        }
    }
}

The flags field is particularly valuable—it categorizes the attack or edge case being tested. Common flags include SignatureMalleability (signatures that verify but differ from canonical form), EdgeCasePublicKey (keys at curve boundaries), SmallResidue (values that could enable small subgroup attacks), and ModifiedSignature (tampered signatures that some implementations incorrectly accept). These flags let you filter tests or track which vulnerability classes your library handles correctly.

Wycheproof's real power comes from its breadth: the repository contains over 80 distinct attack patterns across 30+ algorithms. The AES-GCM tests alone cover authentication tag truncation, IV reuse detection, and empty ciphertext edge cases. RSA PKCS#1 v1.5 tests include multiple Bleichenbacher attack variants that differ in subtle ways—some use leading zeros in padding, others exploit parser ambiguities in ASN.1 encoding. These are the implementation details that code review rarely catches but that attackers exploit reliably.

The project recently standardized on JSON Schema definitions for each algorithm, making integration more robust. The schemas define exact field types, hex encoding requirements, and optional fields. This means you can validate the test vectors themselves before running them, catching integration bugs where you've misinterpreted a field's format. For maintainers, schemas enable automated CI validation of new test contributions, ensuring consistency across the growing test corpus.

Gotcha

Wycheproof's language-agnostic design creates integration friction. You're responsible for writing all the glue code between JSON vectors and your library's API, which means parsing hex strings, handling different key encodings, and mapping algorithm names to your implementation's function calls. For complex algorithms like RSA-OAEP with multiple hash function options, this mapping code can become substantial. The repository provides examples in the doc/ directory, but they're minimal—pyca/cryptography's integration code spans hundreds of lines to handle all the algorithms they support.

The test vectors also expose a philosophical challenge: what does "acceptable" really mean? Wycheproof uses three result values: valid (must accept), invalid (must reject), and acceptable (implementation-defined behavior). That third category covers cases where specs are ambiguous or where security/compatibility tradeoffs exist. For instance, should ECDSA accept signatures with high S values? RFC 6979 says they're valid, but Bitcoin rejects them to prevent transaction malleability. Wycheproof marks these as "acceptable," leaving the decision to you—but now you need a policy document explaining your library's stance on dozens of these edge cases. Some teams struggle with this, wanting clear security guidance that Wycheproof deliberately avoids providing.

Finally, coverage gaps remain. While core algorithms like ECDSA and AES-GCM have exhaustive test suites, newer or less common algorithms may have limited vectors. The post-quantum algorithms (ML-KEM, ML-DSA) were added recently and don't yet have the depth of attack patterns that RSA tests offer. If you're implementing a niche algorithm, you may still need to supplement with your own adversarial test cases.

Verdict

Use if: You're developing or maintaining any cryptographic library that other software depends on—whether that's a general-purpose crypto library like BoringSSL or a specialized implementation of a single algorithm. Wycheproof integration should be the first CI check you set up after basic correctness tests. Also use it if you're auditing a crypto library and want to quickly verify its resilience against known attacks without manually crafting test cases. Skip if: You're building application-level code that simply consumes well-vetted crypto libraries rather than implementing primitives yourself. Wycheproof tests libraries, not applications. Also skip if you need active fuzzing or side-channel testing—Wycheproof covers known attack patterns but won't discover novel vulnerabilities or timing leaks. For those needs, combine it with tools like libFuzzer and dudect respectively.