Skip to content

Commit d4cc872

Browse files
authored
Integrate Wycheproof ML-KEM test vectors (#2891)
### Description of changes: Integrates 9 Wycheproof ML-KEM test vector files: - 3 ML-KEM encapsulation test files (mlkem_512_encaps_test, mlkem_768_encaps_test, mlkem_1024_encaps_test) - 3 ML-KEM test files (mlkem_512_test, mlkem_768_test, mlkem_1024_test) - 3 ML-KEM decapsulation test files (mlkem_512_semi_expanded_decaps_test, mlkem_768_semi_expanded_decaps_test, mlkem_1024_semi_expanded_decaps_test) Each integration adds upstream JSON vectors and converted txt files to `third_party/vectors/`, and adds test code with duvet annotations for traceability. ### Call-outs: - **Generated new test vectors**: the ML-KEM decapsulation test vectors (`mlkem_[512/768/1024]thu_semi_expanded_decaps_test`) are new, and have been merged into upstream C2SP/wycheproof#202. Adds `util/vecgen` that we used to generate the test vectors. - **Missing encaps key import checks**: we successfully import ML-KEM encapsulation keys with modulus overflow. This is allowed by FIPS 203, but is not ideal, so the tests print a warning. We will resolve this in an upcoming PR. - **Missing decaps key import checks**: we successfully import ML-KEM decapsulation keys with an inconsistent hash of the embedded encaps key. This is also allowed by FIPS 203, so the tests print a warning, and we will resolve this in an upcoming PR. ### Testing: All new tests pass and duvet verification succeeds: ```bash cd build && ./crypto/crypto_test --gtest_filter="*Wycheproof*" cd third_party/vectors && python3 sync.py ``` By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and the ISC license. --------- Signed-off-by: sanketh <sgmenda@amazon.com>
1 parent 7487ad1 commit d4cc872

32 files changed

+26106
-1
lines changed

crypto/evp_extra/p_kem_test.cc

Lines changed: 260 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0 OR ISC
33

4+
#include <algorithm>
45
#include <gtest/gtest.h>
56
#include <openssl/base.h>
67
#include <openssl/bio.h>
78
#include <openssl/evp.h>
9+
#include <openssl/experimental/kem_deterministic_api.h>
810
#include <openssl/mem.h>
911
#include <openssl/pem.h>
1012
#include <openssl/pkcs8.h>
1113
#include <openssl/ssl.h>
1214
#include "../fipsmodule/evp/internal.h"
1315
#include "../fipsmodule/kem/internal.h"
16+
#include "../test/file_test.h"
1417
#include "../test/test_util.h"
15-
#include <openssl/experimental/kem_deterministic_api.h>
18+
#include "../test/wycheproof_util.h"
1619

1720

1821
// https://datatracker.ietf.org/doc/draft-ietf-lamps-kyber-certificates/
@@ -927,3 +930,259 @@ TEST(KEMTest, InvalidSeedLength) {
927930

928931
OPENSSL_free(der_priv);
929932
}
933+
934+
935+
// Wycheproof test vector mapping for KEMs
936+
struct WycheproofKEM {
937+
const char name[20];
938+
const int nid;
939+
size_t ciphertext_len;
940+
size_t shared_secret_len;
941+
const char *encaps_test;
942+
const char *decaps_seed_test;
943+
const char *decaps_noseed_test;
944+
};
945+
946+
//= third_party/vectors/vectors_spec.md#wycheproof
947+
//# AWS-LC MUST test against `testvectors_v1/mlkem_1024_test.txt`.
948+
//# AWS-LC MUST test against `testvectors_v1/mlkem_512_test.txt`.
949+
//# AWS-LC MUST test against `testvectors_v1/mlkem_768_test.txt`.
950+
//# AWS-LC MUST test against `testvectors_v1/mlkem_1024_encaps_test.txt`.
951+
//# AWS-LC MUST test against `testvectors_v1/mlkem_1024_semi_expanded_decaps_test.txt`.
952+
//# AWS-LC MUST test against `testvectors_v1/mlkem_512_encaps_test.txt`.
953+
//# AWS-LC MUST test against `testvectors_v1/mlkem_512_semi_expanded_decaps_test.txt`.
954+
//# AWS-LC MUST test against `testvectors_v1/mlkem_768_encaps_test.txt`.
955+
//# AWS-LC MUST test against `testvectors_v1/mlkem_768_semi_expanded_decaps_test.txt`.
956+
static const struct WycheproofKEM kWycheproofKEMs[] = {
957+
{
958+
"ML-KEM-512",
959+
NID_MLKEM512,
960+
768,
961+
32,
962+
"mlkem_512_encaps_test.txt",
963+
"mlkem_512_test.txt",
964+
"mlkem_512_semi_expanded_decaps_test.txt",
965+
},
966+
{
967+
"ML-KEM-768",
968+
NID_MLKEM768,
969+
1088,
970+
32,
971+
"mlkem_768_encaps_test.txt",
972+
"mlkem_768_test.txt",
973+
"mlkem_768_semi_expanded_decaps_test.txt",
974+
},
975+
{
976+
"ML-KEM-1024",
977+
NID_MLKEM1024,
978+
1568,
979+
32,
980+
"mlkem_1024_encaps_test.txt",
981+
"mlkem_1024_test.txt",
982+
"mlkem_1024_semi_expanded_decaps_test.txt",
983+
},
984+
};
985+
986+
class WycheproofKEMTest : public testing::TestWithParam<WycheproofKEM> {};
987+
988+
INSTANTIATE_TEST_SUITE_P(
989+
All, WycheproofKEMTest, testing::ValuesIn(kWycheproofKEMs),
990+
[](const testing::TestParamInfo<WycheproofKEM> &params) -> std::string {
991+
std::string name = params.param.name;
992+
// Replace dashes with underscores for valid C++ test names
993+
std::replace(name.begin(), name.end(), '-', '_');
994+
return name;
995+
});
996+
997+
TEST_P(WycheproofKEMTest, Encaps) {
998+
std::string test_path =
999+
std::string(kWycheproofV1Path) + GetParam().encaps_test;
1000+
FileTestGTest(test_path.c_str(), [&](FileTest *t) {
1001+
std::vector<uint8_t> ek, m, expected_k, expected_c;
1002+
std::string param_set;
1003+
1004+
ASSERT_TRUE(t->GetInstruction(&param_set, "parameterSet"));
1005+
ASSERT_EQ(param_set, GetParam().name);
1006+
1007+
ASSERT_TRUE(t->GetBytes(&ek, "ek"));
1008+
ASSERT_TRUE(t->GetBytes(&m, "m"));
1009+
ASSERT_TRUE(t->GetBytes(&expected_k, "K"));
1010+
ASSERT_TRUE(t->GetBytes(&expected_c, "c"));
1011+
1012+
WycheproofResult result;
1013+
ASSERT_TRUE(GetWycheproofResult(t, &result));
1014+
1015+
bssl::UniquePtr<EVP_PKEY> pkey(
1016+
EVP_PKEY_kem_new_raw_public_key(GetParam().nid, ek.data(), ek.size()));
1017+
1018+
if (!result.IsValid() && result.HasFlag("ModulusOverflow")) {
1019+
if (pkey) {
1020+
// FIPS 203 only requires doing this check before encapsulation.
1021+
fprintf(stderr,
1022+
"WARNING: Successfully imported %s encapsulation key with "
1023+
"ModulusOverflow. This is allowed by FIPS 203.\n",
1024+
param_set.c_str());
1025+
}
1026+
}
1027+
if (pkey) {
1028+
bssl::UniquePtr<EVP_PKEY_CTX> ctx(EVP_PKEY_CTX_new(pkey.get(), nullptr));
1029+
ASSERT_TRUE(ctx);
1030+
1031+
// Perform deterministic encapsulation using the m field as seed
1032+
// see https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.pdf#algorithm.17
1033+
std::vector<uint8_t> ciphertext(GetParam().ciphertext_len);
1034+
std::vector<uint8_t> shared_secret(GetParam().shared_secret_len);
1035+
size_t ciphertext_len = ciphertext.size();
1036+
size_t shared_secret_len = shared_secret.size();
1037+
size_t seed_len = m.size();
1038+
int encaps_result =
1039+
EVP_PKEY_encapsulate_deterministic(ctx.get(), ciphertext.data(), &ciphertext_len,
1040+
shared_secret.data(), &shared_secret_len, m.data(), &seed_len);
1041+
1042+
if (result.IsValid()) {
1043+
EXPECT_TRUE(encaps_result);
1044+
EXPECT_EQ(Bytes(ciphertext.data(), ciphertext_len), Bytes(expected_c));
1045+
EXPECT_EQ(Bytes(shared_secret.data(), shared_secret_len),
1046+
Bytes(expected_k));
1047+
} else {
1048+
EXPECT_FALSE(encaps_result)
1049+
<< "Expected encapsulation to fail for flags: "
1050+
<< result.StringifyFlags();
1051+
}
1052+
}
1053+
});
1054+
}
1055+
1056+
TEST_P(WycheproofKEMTest, DecapsSeed) {
1057+
std::string test_path =
1058+
std::string(kWycheproofV1Path) + GetParam().decaps_seed_test;
1059+
FileTestGTest(test_path.c_str(), [&](FileTest *t) {
1060+
std::vector<uint8_t> ek, seed, expected_k, ciphertext;
1061+
std::string param_set;
1062+
1063+
ASSERT_TRUE(t->GetInstruction(&param_set, "parameterSet"));
1064+
ASSERT_EQ(param_set, GetParam().name);
1065+
1066+
ASSERT_TRUE(t->GetBytes(&expected_k, "K"));
1067+
ASSERT_TRUE(t->GetBytes(&ciphertext, "c"));
1068+
1069+
WycheproofResult result;
1070+
ASSERT_TRUE(GetWycheproofResult(t, &result));
1071+
ASSERT_TRUE(t->GetBytes(&seed, "seed"));
1072+
1073+
// Initialize using provided seed
1074+
bssl::UniquePtr<EVP_PKEY_CTX> ctx(
1075+
EVP_PKEY_CTX_new_id(EVP_PKEY_KEM, nullptr));
1076+
ASSERT_TRUE(ctx);
1077+
ASSERT_TRUE(EVP_PKEY_CTX_kem_set_params(ctx.get(), GetParam().nid));
1078+
EVP_PKEY *raw = nullptr;
1079+
ASSERT_TRUE(EVP_PKEY_keygen_init(ctx.get()));
1080+
size_t seed_len = seed.size();
1081+
int keygen_result = EVP_PKEY_keygen_deterministic(ctx.get(), &raw, seed.data(), &seed_len);
1082+
1083+
// For invalid test cases, key generation might fail
1084+
if (!result.IsValid() && !keygen_result) {
1085+
// Expected failure in key generation for invalid cases
1086+
return;
1087+
}
1088+
1089+
ASSERT_TRUE(keygen_result);
1090+
ASSERT_TRUE(raw);
1091+
bssl::UniquePtr<EVP_PKEY> pkey(raw);
1092+
1093+
// Verify the generated public key matches the expected public key (if provided)
1094+
if (t->HasAttribute("ek")) {
1095+
ASSERT_TRUE(t->GetBytes(&ek, "ek"));
1096+
size_t actual_ek_len = 0;
1097+
ASSERT_TRUE(
1098+
EVP_PKEY_get_raw_public_key(pkey.get(), nullptr, &actual_ek_len));
1099+
ASSERT_EQ(actual_ek_len, ek.size());
1100+
std::vector<uint8_t> actual_ek(actual_ek_len);
1101+
ASSERT_TRUE(EVP_PKEY_get_raw_public_key(pkey.get(), actual_ek.data(),
1102+
&actual_ek_len));
1103+
EXPECT_EQ(Bytes(actual_ek), Bytes(ek));
1104+
}
1105+
1106+
// Perform decapsulation
1107+
ctx.reset(EVP_PKEY_CTX_new(pkey.get(), nullptr));
1108+
ASSERT_TRUE(ctx);
1109+
std::vector<uint8_t> shared_secret(GetParam().shared_secret_len);
1110+
size_t shared_secret_len = shared_secret.size();
1111+
int decaps_result = EVP_PKEY_decapsulate(
1112+
ctx.get(), shared_secret.data(), &shared_secret_len, ciphertext.data(),
1113+
ciphertext.size());
1114+
1115+
if (result.IsValid()) {
1116+
EXPECT_TRUE(decaps_result);
1117+
EXPECT_EQ(Bytes(shared_secret.data(), shared_secret_len),
1118+
Bytes(expected_k));
1119+
} else {
1120+
EXPECT_FALSE(decaps_result)
1121+
<< "Expected decapsulation to fail for flags: "
1122+
<< result.StringifyFlags();
1123+
}
1124+
});
1125+
}
1126+
1127+
// Test decapsulation with expanded decaps keys
1128+
TEST_P(WycheproofKEMTest, DecapsNoSeed) {
1129+
std::string test_path =
1130+
std::string(kWycheproofV1Path) + GetParam().decaps_noseed_test;
1131+
FileTestGTest(test_path.c_str(), [&](FileTest *t) {
1132+
std::vector<uint8_t> dk, ciphertext;
1133+
std::string param_set;
1134+
1135+
ASSERT_TRUE(t->GetInstruction(&param_set, "parameterSet"));
1136+
ASSERT_EQ(param_set, GetParam().name);
1137+
1138+
ASSERT_TRUE(t->GetBytes(&dk, "dk"));
1139+
ASSERT_TRUE(t->GetBytes(&ciphertext, "c"));
1140+
1141+
WycheproofResult result;
1142+
ASSERT_TRUE(GetWycheproofResult(t, &result));
1143+
1144+
// Create key from raw private key bytes
1145+
bssl::UniquePtr<EVP_PKEY> pkey(
1146+
EVP_PKEY_kem_new_raw_secret_key(GetParam().nid, dk.data(), dk.size()));
1147+
1148+
// Key creation should fail for incorrect key length
1149+
if (result.HasFlag("IncorrectDecapsulationKeyLength")) {
1150+
EXPECT_FALSE(pkey)
1151+
<< "Expected key creation to fail for incorrect key length";
1152+
return;
1153+
}
1154+
1155+
// Warn if we successfully imported an invalid private key
1156+
if (pkey && result.HasFlag("InvalidDecapsulationKey")) {
1157+
fprintf(stderr,
1158+
"WARNING: Successfully imported correct-length-but-invalid %s "
1159+
"decapsulation key. This is allowed by FIPS 203.\n",
1160+
param_set.c_str());
1161+
}
1162+
1163+
// For valid test cases, key creation should succeed
1164+
if (result.IsValid()) {
1165+
ASSERT_TRUE(pkey) << "Key creation failed unexpectedly for flags: "
1166+
<< result.StringifyFlags();
1167+
}
1168+
1169+
// Perform decapsulation
1170+
bssl::UniquePtr<EVP_PKEY_CTX> ctx(EVP_PKEY_CTX_new(pkey.get(), nullptr));
1171+
ASSERT_TRUE(ctx);
1172+
1173+
std::vector<uint8_t> shared_secret(GetParam().shared_secret_len);
1174+
size_t shared_secret_len = shared_secret.size();
1175+
int decaps_result = EVP_PKEY_decapsulate(
1176+
ctx.get(), shared_secret.data(), &shared_secret_len, ciphertext.data(),
1177+
ciphertext.size());
1178+
1179+
if (result.IsValid()) {
1180+
EXPECT_TRUE(decaps_result)
1181+
<< "Expected decapsulation to succeed for valid test case";
1182+
} else {
1183+
EXPECT_FALSE(decaps_result)
1184+
<< "Expected decapsulation to fail for flags: "
1185+
<< result.StringifyFlags();
1186+
}
1187+
});
1188+
}

crypto/test/wycheproof_util.cc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ bool WycheproofResult::IsValid(
4747
abort();
4848
}
4949

50+
bool WycheproofResult::HasFlag(const std::string &flag) const {
51+
return std::find(flags.begin(), flags.end(), flag) != flags.end();
52+
}
53+
54+
std::string WycheproofResult::StringifyFlags() {
55+
std::string flags_str;
56+
for (size_t i = 0; i < flags.size(); i++) {
57+
if (i > 0) {
58+
flags_str += ", ";
59+
}
60+
flags_str += flags[i];
61+
}
62+
return flags_str;
63+
}
64+
5065
bool GetWycheproofResult(FileTest *t, WycheproofResult *out) {
5166
std::string result;
5267
if (!t->GetAttribute(&result, "result")) {

crypto/test/wycheproof_util.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323

2424
// This header contains convenience functions for Wycheproof tests.
2525

26+
static constexpr const char kWycheproofV1Path[] =
27+
"third_party/vectors/converted/wycheproof/testvectors_v1/";
28+
2629
class FileTest;
2730

2831
enum class WycheproofRawResult {
@@ -39,6 +42,12 @@ struct WycheproofResult {
3942
// test result of "acceptable" is treated as valid if all flags are included
4043
// in |acceptable_flags| and invalid otherwise.
4144
bool IsValid(const std::vector<std::string> &acceptable_flags = {}) const;
45+
46+
// StringifyFlags returns a printable string of all flags.
47+
std::string StringifyFlags();
48+
49+
// HasFlag returns true if |flag| is present in the flags vector.
50+
bool HasFlag(const std::string &flag) const;
4251
};
4352

4453
// GetWycheproofResult sets |*out| to the parsed "result" and "flags" keys of |t|.
2.61 MB
Binary file not shown.

sources.cmake

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,4 +377,13 @@ set(
377377
third_party/vectors/converted/wycheproof/testvectors_v1/rsa_signature_8192_sha512_test.txt
378378
third_party/wycheproof_testvectors/x25519_test.txt
379379
third_party/wycheproof_testvectors/xchacha20_poly1305_test.txt
380+
third_party/vectors/converted/wycheproof/testvectors_v1/mlkem_512_encaps_test.txt
381+
third_party/vectors/converted/wycheproof/testvectors_v1/mlkem_512_semi_expanded_decaps_test.txt
382+
third_party/vectors/converted/wycheproof/testvectors_v1/mlkem_512_test.txt
383+
third_party/vectors/converted/wycheproof/testvectors_v1/mlkem_768_encaps_test.txt
384+
third_party/vectors/converted/wycheproof/testvectors_v1/mlkem_768_semi_expanded_decaps_test.txt
385+
third_party/vectors/converted/wycheproof/testvectors_v1/mlkem_768_test.txt
386+
third_party/vectors/converted/wycheproof/testvectors_v1/mlkem_1024_encaps_test.txt
387+
third_party/vectors/converted/wycheproof/testvectors_v1/mlkem_1024_semi_expanded_decaps_test.txt
388+
third_party/vectors/converted/wycheproof/testvectors_v1/mlkem_1024_test.txt
380389
)

third_party/vectors/.duvet/snapshot.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ SPECIFICATION: [Test Vector Specification](vectors_spec.md)
1616
TEXT[test]: AWS-LC MUST test against `testvectors_v1/ecdsa_secp384r1_sha512_test.txt`.
1717
TEXT[test]: AWS-LC MUST test against `testvectors_v1/ecdsa_secp521r1_sha512_test.txt`.
1818
TEXT[test]: AWS-LC MUST test against `testvectors_v1/ed25519_test.txt`.
19+
TEXT[test]: AWS-LC MUST test against `testvectors_v1/mlkem_1024_test.txt`.
20+
TEXT[test]: AWS-LC MUST test against `testvectors_v1/mlkem_512_test.txt`.
21+
TEXT[test]: AWS-LC MUST test against `testvectors_v1/mlkem_768_test.txt`.
22+
TEXT[test]: AWS-LC MUST test against `testvectors_v1/mlkem_1024_encaps_test.txt`.
23+
TEXT[test]: AWS-LC MUST test against `testvectors_v1/mlkem_1024_semi_expanded_decaps_test.txt`.
24+
TEXT[test]: AWS-LC MUST test against `testvectors_v1/mlkem_512_encaps_test.txt`.
25+
TEXT[test]: AWS-LC MUST test against `testvectors_v1/mlkem_512_semi_expanded_decaps_test.txt`.
26+
TEXT[test]: AWS-LC MUST test against `testvectors_v1/mlkem_768_encaps_test.txt`.
27+
TEXT[test]: AWS-LC MUST test against `testvectors_v1/mlkem_768_semi_expanded_decaps_test.txt`.
1928
TEXT[test]: AWS-LC MUST test against `testvectors_v1/rsa_pkcs1_1024_sig_gen_test.txt`.
2029
TEXT[test]: AWS-LC MUST test against `testvectors_v1/rsa_pkcs1_1536_sig_gen_test.txt`.
2130
TEXT[test]: AWS-LC MUST test against `testvectors_v1/rsa_pkcs1_2048_sig_gen_test.txt`.
@@ -55,6 +64,15 @@ SPECIFICATION: [Test Vector Specification](vectors_spec.md)
5564
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/ecdsa_secp384r1_sha512_test.txt`.
5665
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/ecdsa_secp521r1_sha512_test.txt`.
5766
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/ed25519_test.txt`.
67+
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/mlkem_1024_test.txt`.
68+
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/mlkem_512_test.txt`.
69+
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/mlkem_768_test.txt`.
70+
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/mlkem_1024_encaps_test.txt`.
71+
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/mlkem_1024_semi_expanded_decaps_test.txt`.
72+
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/mlkem_512_encaps_test.txt`.
73+
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/mlkem_512_semi_expanded_decaps_test.txt`.
74+
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/mlkem_768_encaps_test.txt`.
75+
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/mlkem_768_semi_expanded_decaps_test.txt`.
5876
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/rsa_pkcs1_1024_sig_gen_test.txt`.
5977
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/rsa_pkcs1_1536_sig_gen_test.txt`.
6078
TEXT[!MUST]: AWS-LC MUST test against `testvectors_v1/rsa_pkcs1_2048_sig_gen_test.txt`.

0 commit comments

Comments
 (0)