-
Notifications
You must be signed in to change notification settings - Fork 144
Add EVP_PKEY_check and EVP_PKEY_public_check for KEMs #2709
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1d332de
a09e674
9f59888
1fa9fe8
d24a116
0616873
f30bfd4
5b01330
c419507
416f603
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2584,15 +2584,12 @@ TEST_P(PerKEMTest, RawKeyOperations) { | |
ASSERT_TRUE(pkey_new); | ||
ASSERT_TRUE(EVP_PKEY_kem_check_key(pkey_new.get())); | ||
|
||
// Not supported for anything but EC and RSA keys | ||
// Test EVP_PKEY_check and EVP_PKEY_public_check | ||
bssl::UniquePtr<EVP_PKEY_CTX> kem_key_ctx( | ||
EVP_PKEY_CTX_new(pkey_new.get(), NULL)); | ||
ASSERT_TRUE(kem_key_ctx); | ||
EXPECT_FALSE(EVP_PKEY_check(kem_key_ctx.get())); | ||
EXPECT_FALSE(EVP_PKEY_public_check((kem_key_ctx.get()))); | ||
ASSERT_EQ((uint16_t)ERR_get_error(), | ||
(uint16_t)EVP_R_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE); | ||
ERR_clear_error(); | ||
EXPECT_TRUE(EVP_PKEY_check(kem_key_ctx.get())); | ||
EXPECT_TRUE(EVP_PKEY_public_check((kem_key_ctx.get()))); | ||
|
||
// ---- 5. Test encaps/decaps with new keys ---- | ||
// Create Alice's context with the new key that has both | ||
|
@@ -2780,6 +2777,156 @@ TEST_P(PerKEMTest, RawKeyOperations) { | |
ASSERT_FALSE(EVP_PKEY_kem_check_key(pkey_new.get())); | ||
} | ||
|
||
TEST_P(PerKEMTest, KEMCheckKeyNegativeTests) { | ||
// ---- 1. Setup phase: generate a valid key for reference ---- | ||
bssl::UniquePtr<EVP_PKEY_CTX> ctx; | ||
ctx = setup_ctx_and_generate_key(GetParam().nid, nullptr, nullptr); | ||
ASSERT_TRUE(ctx); | ||
|
||
EVP_PKEY *valid_pkey = EVP_PKEY_CTX_get0_pkey(ctx.get()); | ||
ASSERT_TRUE(valid_pkey); | ||
|
||
// Test EVP_PKEY_check and EVP_PKEY_public_check on valid key | ||
bssl::UniquePtr<EVP_PKEY_CTX> kem_key_ctx(EVP_PKEY_CTX_new(valid_pkey, nullptr)); | ||
ASSERT_TRUE(kem_key_ctx); | ||
EXPECT_TRUE(EVP_PKEY_check(kem_key_ctx.get())); | ||
EXPECT_TRUE(EVP_PKEY_public_check(kem_key_ctx.get())); | ||
|
||
// ---- 2. Test with corrupted public key ---- | ||
// Extract the raw keys from the valid key | ||
size_t pk_len = GetParam().public_key_len; | ||
size_t sk_len = GetParam().secret_key_len; | ||
std::vector<uint8_t> pk_copy(pk_len); | ||
std::vector<uint8_t> sk_copy(sk_len); | ||
|
||
ASSERT_TRUE(EVP_PKEY_get_raw_public_key(valid_pkey, pk_copy.data(), &pk_len)); | ||
ASSERT_TRUE(EVP_PKEY_get_raw_private_key(valid_pkey, sk_copy.data(), &sk_len)); | ||
|
||
// Create a corrupted public key | ||
std::vector<uint8_t> corrupted_pk = pk_copy; | ||
corrupted_pk[0] = 0xFF; | ||
corrupted_pk[1] = 0xFF; | ||
if (pk_len > 2) { | ||
corrupted_pk[2] = 0xFF; | ||
} | ||
Comment on lines
+2808
to
+2811
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isn't it enough to corrupt a single bit? why do we change two more bytes? |
||
|
||
// Create EVP_PKEY with corrupted public key but valid secret key | ||
bssl::UniquePtr<EVP_PKEY> corrupted_pk_pkey( | ||
EVP_PKEY_kem_new_raw_key(GetParam().nid, corrupted_pk.data(), pk_len, | ||
sk_copy.data(), sk_len)); | ||
ASSERT_TRUE(corrupted_pk_pkey); | ||
|
||
// This should fail EVP_PKEY_check due to invalid public key | ||
bssl::UniquePtr<EVP_PKEY_CTX> corrupted_pk_ctx( | ||
EVP_PKEY_CTX_new(corrupted_pk_pkey.get(), nullptr)); | ||
ASSERT_TRUE(corrupted_pk_ctx); | ||
EXPECT_FALSE(EVP_PKEY_check(corrupted_pk_ctx.get())); | ||
EXPECT_FALSE(EVP_PKEY_public_check(corrupted_pk_ctx.get())); | ||
|
||
// ---- 3. Test with corrupted secret key ---- | ||
// Create a corrupted secret key | ||
std::vector<uint8_t> corrupted_sk = sk_copy; | ||
|
||
// Corrupt the last 64 bytes of secret key (in ML-KEM this is the hash) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't corrupting any single bit in the secret key be sufficient? |
||
// This should cause the secret key validation to fail | ||
if (sk_len >= 64) { | ||
corrupted_sk[sk_len - 64] ^= 1; | ||
corrupted_sk[sk_len - 63] ^= 1; | ||
} | ||
|
||
// Create EVP_PKEY with valid public key but corrupted secret key | ||
bssl::UniquePtr<EVP_PKEY> corrupted_sk_pkey( | ||
EVP_PKEY_kem_new_raw_key(GetParam().nid, pk_copy.data(), pk_len, | ||
corrupted_sk.data(), sk_len)); | ||
ASSERT_TRUE(corrupted_sk_pkey); | ||
|
||
// This should fail EVP_PKEY_check due to invalid secret key | ||
bssl::UniquePtr<EVP_PKEY_CTX> corrupted_sk_ctx( | ||
EVP_PKEY_CTX_new(corrupted_sk_pkey.get(), nullptr)); | ||
ASSERT_TRUE(corrupted_sk_ctx); | ||
EXPECT_FALSE(EVP_PKEY_check(corrupted_sk_ctx.get())); | ||
// Public key check will fail PCT since secret key is present, and corrupted | ||
EXPECT_FALSE(EVP_PKEY_public_check(corrupted_sk_ctx.get())); | ||
|
||
// ---- 4. Test mismatched keypair (PCT failure) ---- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a differentiation between mismatched keypair and corrupted key pare cases above? is a mismatched key pair going to fail differently than a corrupted key pair? |
||
// Generate a second key pair to create a mismatch | ||
bssl::UniquePtr<EVP_PKEY_CTX> ctx2; | ||
ctx2 = setup_ctx_and_generate_key(GetParam().nid, nullptr, nullptr); | ||
ASSERT_TRUE(ctx2); | ||
|
||
EVP_PKEY *second_pkey = EVP_PKEY_CTX_get0_pkey(ctx2.get()); | ||
ASSERT_TRUE(second_pkey); | ||
|
||
// Extract secret key from second keypair | ||
std::vector<uint8_t> second_sk_copy(sk_len); | ||
size_t second_sk_len = sk_len; | ||
ASSERT_TRUE(EVP_PKEY_get_raw_private_key(second_pkey, second_sk_copy.data(), &second_sk_len)); | ||
|
||
// Create EVP_PKEY with public key from first keypair and secret key from second keypair | ||
bssl::UniquePtr<EVP_PKEY> mismatched_pkey( | ||
EVP_PKEY_kem_new_raw_key(GetParam().nid, pk_copy.data(), pk_len, | ||
second_sk_copy.data(), sk_len)); | ||
ASSERT_TRUE(mismatched_pkey); | ||
|
||
// This should fail EVP_PKEY_check due to mismatched keypair (PCT failure) | ||
bssl::UniquePtr<EVP_PKEY_CTX> mismatched_ctx( | ||
EVP_PKEY_CTX_new(mismatched_pkey.get(), nullptr)); | ||
ASSERT_TRUE(mismatched_ctx); | ||
EXPECT_FALSE(EVP_PKEY_check(mismatched_ctx.get())); | ||
// Public key check will fail PCT | ||
EXPECT_FALSE(EVP_PKEY_public_check(mismatched_ctx.get())); | ||
|
||
// ---- 5. Test with public key only ---- | ||
// Create EVP_PKEY with only public key (no secret key) | ||
bssl::UniquePtr<EVP_PKEY> public_only_pkey( | ||
EVP_PKEY_kem_new_raw_public_key(GetParam().nid, pk_copy.data(), pk_len)); | ||
ASSERT_TRUE(public_only_pkey); | ||
|
||
bssl::UniquePtr<EVP_PKEY_CTX> public_only_ctx( | ||
EVP_PKEY_CTX_new(public_only_pkey.get(), nullptr)); | ||
ASSERT_TRUE(public_only_ctx); | ||
|
||
// Both checks should pass | ||
EXPECT_TRUE(EVP_PKEY_check(public_only_ctx.get())); | ||
EXPECT_TRUE(EVP_PKEY_public_check(public_only_ctx.get())); | ||
|
||
// ---- 6. Test with corrupted public key (public key only) ---- | ||
bssl::UniquePtr<EVP_PKEY> corrupted_public_only_pkey( | ||
EVP_PKEY_kem_new_raw_public_key(GetParam().nid, corrupted_pk.data(), pk_len)); | ||
ASSERT_TRUE(corrupted_public_only_pkey); | ||
|
||
bssl::UniquePtr<EVP_PKEY_CTX> corrupted_public_only_ctx( | ||
EVP_PKEY_CTX_new(corrupted_public_only_pkey.get(), nullptr)); | ||
ASSERT_TRUE(corrupted_public_only_ctx); | ||
|
||
// Both checks should fail due to invalid public key | ||
EXPECT_FALSE(EVP_PKEY_check(corrupted_public_only_ctx.get())); | ||
EXPECT_FALSE(EVP_PKEY_public_check(corrupted_public_only_ctx.get())); | ||
|
||
// ---- 7. Test with secret key only (no public key) ---- | ||
// Create EVP_PKEY with only secret key (no public key) | ||
bssl::UniquePtr<EVP_PKEY> secret_only_pkey( | ||
EVP_PKEY_kem_new_raw_secret_key(GetParam().nid, sk_copy.data(), sk_len)); | ||
ASSERT_TRUE(secret_only_pkey); | ||
|
||
bssl::UniquePtr<EVP_PKEY_CTX> secret_only_ctx( | ||
EVP_PKEY_CTX_new(secret_only_pkey.get(), nullptr)); | ||
ASSERT_TRUE(secret_only_ctx); | ||
|
||
// Both checks should fail because public key is required | ||
EXPECT_FALSE(EVP_PKEY_check(secret_only_ctx.get())); | ||
EXPECT_FALSE(EVP_PKEY_public_check(secret_only_ctx.get())); | ||
|
||
// ---- 8. Verify original valid key still works ---- | ||
// Make sure our tests didn't affect the original valid key | ||
EXPECT_TRUE(EVP_PKEY_check(kem_key_ctx.get())); | ||
EXPECT_TRUE(EVP_PKEY_public_check(kem_key_ctx.get())); | ||
|
||
// Clear any remaining errors | ||
ERR_clear_error(); | ||
} | ||
|
||
|
||
// Perform Known Answer Test (KAT) on known KEMs. | ||
// These tests access the deterministic EVP APIs for KeyGen and Encapsulation. | ||
// To perform KATs in KEMs we use a DRBG seeded with a given state "seed". | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -356,3 +356,107 @@ int KEM_KEY_set_raw_keypair_from_seed(KEM_KEY *key, const CBS *seed) { | |
|
||
return 1; | ||
} | ||
|
||
// KEM_check_key: validates the key based on available key material | ||
// - If only public key: validates public key only | ||
// - If secret key is present: requires public key and validates public key, secret key, and PCT | ||
int KEM_check_key(const KEM_KEY *key) { | ||
if (key == NULL) { | ||
OPENSSL_PUT_ERROR(EVP, ERR_R_PASSED_NULL_PARAMETER); | ||
return 0; | ||
} | ||
|
||
// Check that the KEM method and parameters are valid | ||
if (key->kem == NULL || key->kem->method == NULL) { | ||
OPENSSL_PUT_ERROR(EVP, EVP_R_NO_KEY_SET); | ||
return 0; | ||
} | ||
|
||
// Must have at least a public key | ||
if (key->public_key == NULL) { | ||
OPENSSL_PUT_ERROR(EVP, EVP_R_NO_KEY_SET); | ||
return 0; | ||
} | ||
|
||
// If we have a secret key, we must also have a public key for PCT | ||
if (key->secret_key != NULL && key->public_key == NULL) { | ||
OPENSSL_PUT_ERROR(EVP, EVP_R_NO_KEY_SET); | ||
return 0; | ||
} | ||
|
||
// Call appropriate ML-KEM check functions based on KEM NID | ||
switch (key->kem->nid) { | ||
case NID_MLKEM512: | ||
case NID_KYBER512_R3: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we should mix-up Kyber with ML-KEM, technically they are not the same algorithms. I'd drop Kyber NIDs from here. |
||
// Always validate public key | ||
if (ml_kem_512_check_pk(key->public_key) != 0) { | ||
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR); | ||
return 0; | ||
} | ||
|
||
// If secret key is present, validate it and perform PCT | ||
if (key->secret_key != NULL) { | ||
if (ml_kem_512_check_sk(key->secret_key) != 0) { | ||
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR); | ||
return 0; | ||
} | ||
|
||
if (ml_kem_512_check_pct(key->public_key, key->secret_key) != 0) { | ||
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR); | ||
return 0; | ||
} | ||
} | ||
break; | ||
|
||
case NID_MLKEM768: | ||
case NID_KYBER768_R3: | ||
// Always validate public key | ||
if (ml_kem_768_check_pk(key->public_key) != 0) { | ||
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR); | ||
return 0; | ||
} | ||
|
||
// If secret key is present, validate it and perform PCT | ||
if (key->secret_key != NULL) { | ||
if (ml_kem_768_check_sk(key->secret_key) != 0) { | ||
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR); | ||
return 0; | ||
} | ||
|
||
if (ml_kem_768_check_pct(key->public_key, key->secret_key) != 0) { | ||
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR); | ||
return 0; | ||
} | ||
} | ||
break; | ||
|
||
case NID_MLKEM1024: | ||
case NID_KYBER1024_R3: | ||
// Always validate public key | ||
if (ml_kem_1024_check_pk(key->public_key) != 0) { | ||
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR); | ||
return 0; | ||
} | ||
|
||
// If secret key is present, validate it and perform PCT | ||
if (key->secret_key != NULL) { | ||
if (ml_kem_1024_check_sk(key->secret_key) != 0) { | ||
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR); | ||
return 0; | ||
} | ||
|
||
if (ml_kem_1024_check_pct(key->public_key, key->secret_key) != 0) { | ||
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR); | ||
return 0; | ||
} | ||
} | ||
break; | ||
|
||
default: | ||
// For unsupported KEM variants | ||
OPENSSL_PUT_ERROR(EVP, EVP_R_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE); | ||
return 0; | ||
} | ||
|
||
return 1; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will produce a flaky test because with some probability the valid keys will have the bytes equal to 0xFF :)
to be sure that you are corrupting the keys you can just XOR with 1 (
^ 1
), that always changes the least significant bit.