Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crypto/evp_extra/evp_asn1.c
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ int EVP_PKEY_check(EVP_PKEY_CTX *ctx) {
}
case EVP_PKEY_RSA:
return RSA_check_key(pkey->pkey.rsa);
case EVP_PKEY_KEM:
return KEM_check_key(pkey->pkey.kem_key);
default:
OPENSSL_PUT_ERROR(EVP, EVP_R_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE);
return 0;
Expand All @@ -357,6 +359,8 @@ int EVP_PKEY_public_check(EVP_PKEY_CTX *ctx) {
return EC_KEY_check_key(pkey->pkey.ec);
case EVP_PKEY_RSA:
return RSA_check_key(pkey->pkey.rsa);
case EVP_PKEY_KEM:
return KEM_check_key(pkey->pkey.kem_key);
default:
OPENSSL_PUT_ERROR(EVP, EVP_R_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE);
return 0;
Expand Down
159 changes: 153 additions & 6 deletions crypto/evp_extra/evp_extra_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Copy link
Contributor

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.

corrupted_pk[1] = 0xFF;
if (pk_len > 2) {
corrupted_pk[2] = 0xFF;
}
Comment on lines +2808 to +2811
Copy link
Contributor

Choose a reason for hiding this comment

The 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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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) ----
Copy link
Contributor

Choose a reason for hiding this comment

The 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".
Expand Down
7 changes: 7 additions & 0 deletions crypto/fipsmodule/kem/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ int KEM_KEY_set_raw_key(KEM_KEY *key, const uint8_t *in_public,
// |key->secret_key| must both be NULL.
int KEM_KEY_set_raw_keypair_from_seed(KEM_KEY *key, const CBS *seed);

// KEM_check_key function validates a KEM 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
//
// NOTE: Returns 1 on success, 0 on failure.
int KEM_check_key(const KEM_KEY *key);

#if defined(__cplusplus)
} // extern C
#endif
Expand Down
104 changes: 104 additions & 0 deletions crypto/fipsmodule/kem/kem.c
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
Loading
Loading