名称: wycheproof 类型: 领域 描述: > Wycheproof 提供测试向量用于验证加密实现。 在测试加密代码以应对已知攻击和边缘情况时使用。
Wycheproof
Wycheproof 是一个广泛的测试向量集合,旨在验证加密实现的正确性并测试已知攻击。最初由 Google 开发,现在是一个社区管理的项目,贡献者可以添加特定加密构造的测试向量。
背景
关键概念
| 概念 | 描述 |
|---|---|
| 测试向量 | 用于验证加密实现正确性的输入/输出对 |
| 测试组 | 共享属性(密钥大小、IV 大小、曲线)的测试向量集合 |
| 结果标志 | 指示测试应通过(有效)、失败(无效)或可接受 |
| 边缘情况测试 | 测试已知漏洞和攻击模式 |
为什么这很重要
加密实现 notoriously difficult to get right。即使小错误也可以:
- 暴露私钥
- 允许签名伪造
- 启用消息解密
- 当不同实现接受/拒绝相同输入时创建共识问题
Wycheproof 在主要库中发现了漏洞,包括 OpenJDK 的 SHA1withDSA、Bouncy Castle 的 ECDHC 和 elliptic npm 包。
何时使用
应用 Wycheproof 当:
- 测试加密实现(AES-GCM、ECDSA、ECDH、RSA 等)
- 验证加密代码正确处理边缘情况
- 针对已知攻击向量验证实现
- 为加密库设置 CI/CD
- 审计第三方加密代码的正确性
考虑替代方案当:
- 测试时序侧信道(使用 constant-time 测试工具代替)
- 发现新的未知错误(使用模糊测试代替)
- 测试自定义/实验性加密算法(Wycheproof 仅覆盖已建立的算法)
快速参考
| 场景 | 推荐方法 | 注释 |
|---|---|---|
| AES-GCM 实现 | 使用 aes_gcm_test.json |
316 个测试向量跨越 44 个测试组 |
| ECDSA 验证 | 使用 ecdsa_*_test.json 用于特定曲线 |
测试签名可塑性、DER 编码 |
| ECDH 密钥交换 | 使用 ecdh_*_test.json |
测试无效曲线攻击 |
| RSA 签名 | 使用 rsa_*_test.json |
测试填充预言攻击 |
| ChaCha20-Poly1305 | 使用 chacha20_poly1305_test.json |
测试 AEAD 实现 |
测试工作流
阶段 1: 设置 阶段 2: 解析测试向量
┌─────────────────┐ ┌─────────────────┐
│ 添加 Wycheproof │ → │ 加载 JSON 文件 │
│ 作为子模块 │ │ 按参数过滤 │
└─────────────────┘ └─────────────────┘
↓ ↓
阶段 4: CI 集成 阶段 3: 编写测试套件
┌─────────────────┐ ┌─────────────────┐
│ 自动更新 │ ← │ 测试有效和 │
│ 测试向量 │ │ 无效案例 │
└─────────────────┘ └─────────────────┘
仓库结构
Wycheproof 仓库组织如下:
┣ 📜 README.md : 项目概述
┣ 📂 doc : 文档
┣ 📂 java : Java JCE 接口测试套件
┣ 📂 javascript : JavaScript 测试套件
┣ 📂 schemas : 测试向量模式
┣ 📂 testvectors : 测试向量
┗ 📂 testvectors_v1 : 更新后的测试向量(更详细)
基本文件夹是 testvectors 和 testvectors_v1。虽然两者包含类似文件,但 testvectors_v1 包括更详细的信息,推荐用于新集成。
支持算法
Wycheproof 为各种加密算法提供测试向量:
| 类别 | 算法 |
|---|---|
| 对称加密 | AES-GCM, AES-EAX, ChaCha20-Poly1305 |
| 签名 | ECDSA, EdDSA, RSA-PSS, RSA-PKCS1 |
| 密钥交换 | ECDH, X25519, X448 |
| 哈希 | HMAC, HKDF |
| 曲线 | secp256k1, secp256r1, secp384r1, secp521r1, ed25519, ed448 |
测试文件结构
每个 JSON 测试文件测试特定加密构造。所有测试文件共享公共属性:
"algorithm" : 测试的算法名称
"schema" : JSON 模式(在 schemas 文件夹中找到)
"generatorVersion" : 版本号
"numberOfTests" : 此文件中的测试向量总数
"header" : 测试向量的详细描述
"notes" : 测试向量中标志的深入解释
"testGroups" : 一个或多个测试组的数组
测试组
测试组基于共享属性分组测试集,如:
- 密钥大小
- IV 大小
- 公钥
- 曲线
此分类允许提取符合正在测试构造特定标准的测试。
测试向量属性
共享属性
所有测试向量包含四个公共字段:
- tcId: 测试向量在文件内的唯一标识符
- comment: 测试案例的附加信息
- flags: 特定测试案例类型和潜在危险的描述(在
notes字段中引用) - result: 测试的预期结果
result 字段可以取三个值:
| 结果 | 含义 |
|---|---|
| 有效 | 测试案例应成功 |
| 可接受 | 测试案例允许成功但包含非理想属性 |
| 无效 | 测试案例应失败 |
唯一属性
唯一属性特定于正在测试的算法:
| 算法 | 唯一属性 |
|---|---|
| AES-GCM | key, iv, aad, msg, ct, tag |
| ECDH secp256k1 | public, private, shared |
| ECDSA | msg, sig, result |
| EdDSA | msg, sig, pk |
实施指南
阶段 1: 将 Wycheproof 添加到您的项目
选项 1: Git 子模块(推荐)
添加 Wycheproof 作为 git 子模块确保自动更新:
git submodule add https://github.com/C2SP/wycheproof.git
选项 2: 获取特定测试向量
如果子模块不可能,获取特定 JSON 文件:
#!/bin/bash
TMP_WYCHEPROOF_FOLDER=".wycheproof/"
TEST_VECTORS=('aes_gcm_test.json' 'aes_eax_test.json')
BASE_URL="https://raw.githubusercontent.com/C2SP/wycheproof/master/testvectors_v1/"
# 创建 wycheproof 文件夹
mkdir -p $TMP_WYCHEPROOF_FOLDER
# 如果不存在,请求所有测试向量文件
for i in "${TEST_VECTORS[@]}"; do
if [ ! -f "${TMP_WYCHEPROOF_FOLDER}${i}" ]; then
curl -o "${TMP_WYCHEPROOF_FOLDER}${i}" "${BASE_URL}${i}"
if [ $? -ne 0 ]; then
echo "Failed to download ${i}"
exit 1
fi
fi
done
阶段 2: 解析测试向量
识别您的算法的测试文件并解析 JSON:
Python 示例:
import json
def load_wycheproof_test_vectors(path: str):
testVectors = []
try:
with open(path, "r") as f:
wycheproof_json = json.loads(f.read())
except FileNotFoundError:
print(f"No Wycheproof file found at: {path}")
return testVectors
# 需要十六进制到字节转换的属性
convert_attr = {"key", "aad", "iv", "msg", "ct", "tag"}
for testGroup in wycheproof_json["testGroups"]:
# 基于实现约束过滤测试组
if testGroup["ivSize"] < 64 or testGroup["ivSize"] > 1024:
continue
for tv in testGroup["tests"]:
# 将十六进制字符串转换为字节
for attr in convert_attr:
if attr in tv:
tv[attr] = bytes.fromhex(tv[attr])
testVectors.append(tv)
return testVectors
JavaScript 示例:
const fs = require('fs').promises;
async function loadWycheproofTestVectors(path) {
const tests = [];
try {
const fileContent = await fs.readFile(path);
const data = JSON.parse(fileContent.toString());
data.testGroups.forEach(testGroup => {
testGroup.tests.forEach(test => {
// 将共享测试组属性添加到每个测试
test['pk'] = testGroup.publicKey.pk;
tests.push(test);
});
});
} catch (err) {
console.error('Error reading or parsing file:', err);
throw err;
}
return tests;
}
阶段 3: 编写测试套件
创建处理有效和无效测试案例的测试函数。
Python/pytest 示例:
import pytest
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
tvs = load_wycheproof_test_vectors("wycheproof/testvectors_v1/aes_gcm_test.json")
@pytest.mark.parametrize("tv", tvs, ids=[str(tv['tcId']) for tv in tvs])
def test_encryption(tv):
try:
aesgcm = AESGCM(tv['key'])
ct = aesgcm.encrypt(tv['iv'], tv['msg'], tv['aad'])
except ValueError as e:
# 实现引发错误 - 验证测试预期失败
assert tv['result'] != 'valid', tv['comment']
return
if tv['result'] == 'valid':
assert ct[:-16] == tv['ct'], f"Ciphertext mismatch: {tv['comment']}"
assert ct[-16:] == tv['tag'], f"Tag mismatch: {tv['comment']}"
elif tv['result'] == 'invalid' or tv['result'] == 'acceptable':
assert ct[:-16] != tv['ct'] or ct[-16:] != tv['tag']
@pytest.mark.parametrize("tv", tvs, ids=[str(tv['tcId']) for tv in tvs])
def test_decryption(tv):
try:
aesgcm = AESGCM(tv['key'])
decrypted_msg = aesgcm.decrypt(tv['iv'], tv['ct'] + tv['tag'], tv['aad'])
except ValueError:
assert tv['result'] != 'valid', tv['comment']
return
except InvalidTag:
assert tv['result'] != 'valid', tv['comment']
assert 'ModifiedTag' in tv['flags'], f"Expected 'ModifiedTag' flag: {tv['comment']}"
return
assert tv['result'] == 'valid', f"No invalid test case should pass: {tv['comment']}"
assert decrypted_msg == tv['msg'], f"Decryption mismatch: {tv['comment']}"
JavaScript/Mocha 示例:
const assert = require('assert');
function testFactory(tcId, tests) {
it(`[${tcId + 1}] ${tests[tcId].comment}`, function () {
const test = tests[tcId];
const ed25519 = new eddsa('ed25519');
const key = ed25519.keyFromPublic(toArray(test.pk, 'hex'));
let sig;
if (test.result === 'valid') {
sig = key.verify(test.msg, test.sig);
assert.equal(sig, true, `[${test.tcId}] ${test.comment}`);
} else if (test.result === 'invalid') {
try {
sig = key.verify(test.msg, test.sig);
} catch (err) {
// 点无法解码
sig = false;
}
assert.equal(sig, false, `[${test.tcId}] ${test.comment}`);
}
});
}
// 为所有测试向量生成测试
for (var tcId = 0; tcId < tests.length; tcId++) {
testFactory(tcId, tests);
}
阶段 4: CI 集成
确保测试向量保持最新通过:
- 使用 git 子模块:在运行测试前更新 CI 中的子模块
- 获取最新向量:在测试执行前运行获取脚本
- 计划更新:设置每周/每月更新以捕获新测试向量
常见检测到的漏洞
Wycheproof 测试向量旨在捕获特定漏洞模式:
| 漏洞 | 描述 | 受影响算法 | 示例 CVE |
|---|---|---|---|
| 签名可塑性 | 同一消息的多个有效签名 | ECDSA, EdDSA | CVE-2024-42459 |
| 无效 DER 编码 | 接受非规范 DER 签名 | ECDSA | CVE-2024-42460, CVE-2024-42461 |
| 无效曲线攻击 | ECDH 与无效曲线点 | ECDH | 许多库中常见 |
| 填充预言 | 填充验证中的时序泄漏 | RSA-PKCS1 | 历史 OpenSSL 问题 |
| 标签伪造 | 接受修改的认证标签 | AES-GCM, ChaCha20-Poly1305 | 各种实现 |
签名可塑性:深入探讨
问题: 不验证签名编码的实现可以接受同一消息的多个有效签名。
示例 (EdDSA): 向签名添加或删除零:
有效签名: ...6a5c51eb6f946b30d
无效签名: ...6a5c51eb6f946b30d0000 (应拒绝)
如何检测:
# 添加签名长度检查
if len(sig) != 128: # EdDSA 签名必须正好是 64 字节(128 十六进制字符)
return False
影响: 当不同实现接受/拒绝相同签名时,可能导致共识问题。
相关 Wycheproof 测试:
- EdDSA: tcId 37 - “从签名中移除 0 字节”
- ECDSA: tcId 06 - “传统:r 的 ASN 编码缺少前导 0”
案例研究:Elliptic npm 包
此案例研究展示了 Wycheproof 如何在流行的 elliptic npm 包(3000+ 依赖,数百万周下载)中发现三个 CVE。
概述
elliptic 库是一个用 JavaScript 编写的椭圆曲线加密库,支持 ECDH、ECDSA 和 EdDSA。在版本 6.5.6 上使用 Wycheproof 测试向量揭示了多个漏洞:
- CVE-2024-42459: EdDSA 签名可塑性(添加/删除零)
- CVE-2024-42460: ECDSA DER 编码 - 无效位放置
- CVE-2024-42461: ECDSA DER 编码 - 长度字段中的前导零
方法论
- 识别支持的曲线:ed25519 用于 EdDSA
- 找到测试向量:
testvectors_v1/ed25519_test.json - 解析测试向量:加载 JSON 并提取测试
- 编写测试套件:创建参数化测试
- 运行测试:识别失败
- 分析根本原因:检查实现代码
- 提出修复:添加验证检查
关键发现
EdDSA 问题 (CVE-2024-42459):
- 缺少签名长度验证
- 允许签名中的尾随零
- 修复:添加
if(sig.length !== 128) return false;
ECDSA 问题 1 (CVE-2024-42460):
- 缺少检查 DER 编码的 r 和 s 值中的第一位为零
- 修复:添加
if ((data[p.place] & 128) !== 0) return false;
ECDSA 问题 2 (CVE-2024-42461):
- DER 长度字段接受前导零
- 修复:添加
if(buf[p.place] === 0x00) return false;
影响
所有三个漏洞都允许单个消息的多个有效签名,导致跨实现的共识问题。
学到的教训:
- Wycheproof 捕获微妙的编码错误
- 可重用测试套件带来好处
- 测试向量注释和标志帮助诊断问题
- 即使流行库也受益于系统测试向量验证
高级用法
提示和技巧
| 提示 | 为什么它有帮助 |
|---|---|
| 按参数过滤测试组 | 关注与实现约束相关的测试向量 |
| 使用测试向量标志 | 理解正在测试的特定漏洞模式 |
检查 notes 字段 |
获取标志含义的详细解释 |
| 测试加密/解密和签名/验证 | 确保双向正确性 |
| 在 CI 中运行测试 | 捕获回归并受益于新测试向量 |
| 使用参数化测试 | 获得带有 tcId 和注释的清晰失败消息 |
常见错误
| 错误 | 为什么它是错误的 | 正确方法 |
|---|---|---|
| 仅测试有效案例 | 错过了接受无效输入的漏洞 | 测试所有结果类型:有效、无效、可接受 |
| 忽略“可接受”结果 | 实现可能有细微错误 | 将可接受视为值得调查的警告 |
| 不过滤测试组 | 浪费在不支持参数上的时间 | 基于实现按 keySize、ivSize 等过滤 |
| 不更新测试向量 | 错过新漏洞模式 | 使用子模块或计划获取 |
| 仅测试一个方向 | 加密/签名可能有效但解密/验证失败 | 测试两个操作 |
相关技能
工具技能
| 技能 | 在 Wycheproof 测试中的主要用途 |
|---|---|
| pytest | 用于参数化测试的 Python 测试框架 |
| mocha | 用于测试生成的 JavaScript 测试框架 |
| constant-time-testing | 用时序侧信道测试补充 Wycheproof |
| cryptofuzz | 基于模糊的加密测试以找到额外错误 |
技术技能
| 技能 | 何时应用 |
|---|---|
| coverage-analysis | 确保测试向量覆盖加密实现中的所有代码路径 |
| property-based-testing | 测试数学属性(例如,加密/解密往返) |
| fuzz-harness-writing | 为加密解析器创建套件(补充 Wycheproof) |
相关领域技能
| 技能 | 关系 |
|---|---|
| crypto-testing | Wycheproof 是综合加密测试方法中的关键工具 |
| fuzzing | 使用模糊测试找到 Wycheproof 未覆盖的错误(新边缘案例) |
技能依赖图
┌─────────────────────┐
│ wycheproof │
│ (此技能) │
└──────────┬──────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ pytest/mocha │ │ constant-time │ │ cryptofuzz │
│ (测试框架) │ │ 测试 │ │ (模糊测试) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────┼───────────────────┘
│
▼
┌──────────────────────────┐
│ 技术技能 │
│ 覆盖、套件、PBT │
└──────────────────────────┘
资源
官方仓库
官方仓库包含:
- 所有测试向量在
testvectors/和testvectors_v1/ - JSON 模式在
schemas/ - Java 和 JavaScript 中的参考实现
- 文档在
doc/
现实世界示例
pycryptodome 库在其测试套件中集成了 Wycheproof 测试向量,展示了 Python 加密实现的最佳实践。
社区资源
- C2SP 社区 - 维护 Wycheproof 的加密规范和标准社区
- Wycheproof 问题跟踪器 - 报告测试向量中的错误或建议新构造
总结
Wycheproof 是验证加密实现对抗已知攻击向量和边缘情况的重要工具。通过将 Wycheproof 测试向量集成到您的测试工作流中:
- 捕获微妙的编码和验证错误
- 防止签名可塑性问题
- 确保跨实现的一致行为
- 受益于社区贡献的测试向量
- 保护免受已知加密漏洞
投资编写可重用测试套件通过 Wycheproof 仓库添加新测试向量时的持续验证带来回报。