Skip to content

Issue #158: HKDF behavior mismatch when passing in nullptr due to OpenSSL changes.#159

Merged
riwoh merged 2 commits into
rdkcentral:mainfrom
seanjin99:HKDF_fix
Jan 16, 2026
Merged

Issue #158: HKDF behavior mismatch when passing in nullptr due to OpenSSL changes.#159
riwoh merged 2 commits into
rdkcentral:mainfrom
seanjin99:HKDF_fix

Conversation

@seanjin99

Copy link
Copy Markdown
Contributor

This openSSL version 3.6.0 1 Oct 2025, OpenSSL's EVP_PKEY_CTX_set1_hkdf_* callers can behave poorly if passed a
NULL pointer even when the length is zero.

@github-actions

github-actions Bot commented Nov 18, 2025

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Signed-off-by: seanjin99 <xhjin99@gmail.com>

@jcarrete5 jcarrete5 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the issue is caused by passing NULL to EVP_PKEY_CTX_set1_hkdf_salt, can we check for NULL and, if it is NULL, skip calling EVP_PKEY_CTX_set1_hkdf_salt altogether? Do we need to always call this function or can it be omitted when we have no salt to provide?

Example:

if (salt.data() != nullptr && EVP_PKEY_CTX_set1_hkdf_salt(pctx.get(), salt.data(), salt_len) <= 0) {
    ERROR("EVP_PKEY_CTX_set1_hkdf_salt failed: salt_len=%d", salt_len);
    return false;
}

@seanjin99

Copy link
Copy Markdown
Contributor Author

If the issue is caused by passing Code BlockCopyNULL to Code BlockCopyEVP_PKEY_CTX_set1_hkdf_salt, can we check for Code BlockCopyNULL and, if it is Code BlockCopyNULL, skip calling Code BlockCopyEVP_PKEY_CTX_set1_hkdf_salt altogether? Do we need to always call this function or can it be omitted when we have no salt to provide?

Example:

if (salt.data() != nullptr && EVP_PKEY_CTX_set1_hkdf_salt(pctx.get(), salt.data(), salt_len) <= 0) {
    ERROR("EVP_PKEY_CTX_set1_hkdf_salt failed: salt_len=%d", salt_len);
    return false;
}

According to RFC 5869 (HKDF spec): Salt is an optional parameter. If not provided, it defaults to a string of zeros of the hash length (e.g., 32 zero bytes for SHA-256).

OpenSSL's behavior:

Calling EVP_PKEY_CTX_set1_hkdf_salt() with a zero-length salt (non-NULL pointer, length=0) explicitly sets an empty salt
NOT calling EVP_PKEY_CTX_set1_hkdf_salt() at all causes OpenSSL to use the RFC-compliant default (hash-length zeros)

in consequence that brings different security implication--- These produce different cryptographic outputs:

#1. Empty salt (length=0): HKDF uses 0 bytes as salt
#2. No salt call: HKDF uses 32 zero bytes (for SHA-256) as salt per RFC 5869

@seanjin99

Copy link
Copy Markdown
Contributor Author

If the issue is caused by passing Code BlockCopyNULL to Code BlockCopyEVP_PKEY_CTX_set1_hkdf_salt, can we check for Code BlockCopyNULL and, if it is Code BlockCopyNULL, skip calling Code BlockCopyEVP_PKEY_CTX_set1_hkdf_salt altogether? Do we need to always call this function or can it be omitted when we have no salt to provide?
Example:

if (salt.data() != nullptr && EVP_PKEY_CTX_set1_hkdf_salt(pctx.get(), salt.data(), salt_len) <= 0) {
    ERROR("EVP_PKEY_CTX_set1_hkdf_salt failed: salt_len=%d", salt_len);
    return false;
}

According to RFC 5869 (HKDF spec): Salt is an optional parameter. If not provided, it defaults to a string of zeros of the hash length (e.g., 32 zero bytes for SHA-256).

OpenSSL's behavior:

Calling EVP_PKEY_CTX_set1_hkdf_salt() with a zero-length salt (non-NULL pointer, length=0) explicitly sets an empty salt NOT calling EVP_PKEY_CTX_set1_hkdf_salt() at all causes OpenSSL to use the RFC-compliant default (hash-length zeros)

in consequence that brings different security implication--- These produce different cryptographic outputs:

#1. Empty salt (length=0): HKDF uses 0 bytes as salt #2. No salt call: HKDF uses 32 zero bytes (for SHA-256) as salt per RFC 5869

For info, the two approaches are cryptographically equivalent:

Empty info (length=0): HKDF uses 0 bytes
No call at all: HKDF uses 0 bytes (RFC 5869 default)
So unlike salt, skipping the EVP_PKEY_CTX_set1_hkdf_info() call when info is empty would not change the cryptographic output.

@jcarrete5

jcarrete5 commented Nov 20, 2025

Copy link
Copy Markdown
Contributor

Hmm, was able to dig up this issue pointing to the potential regression: openssl/openssl#28200

I created a simple test:

const auto key_size = 16;
std::vector<uint8_t> salt{};
std::vector<uint8_t> info{};
std::vector<uint8_t> clear_key(key_size);
std::vector<uint8_t> clear_base_key{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
ASSERT_TRUE(hkdf(clear_key, clear_base_key, salt, info, SA_DIGEST_ALGORITHM_SHA256));
const std::vector<uint8_t> expected_key{0x74, 0xb2, 0, 0x5f, 0xb0, 0x20, 0xa3, 0xe0, 0x1b, 0xe2, 0xb2, 0x9d, 0xe4, 0x29, 0xb3, 0xd5};
ASSERT_EQ(clear_key, expected_key);

and modified hkdf to call EVP_PKEY_CTX_set1_hkdf_salt six different ways when the given salt is empty:

  1. Using a pointer to 0 for the salt argument with zero length
  2. Using a pointer to 1 for the salt argument with zero length
  3. Completely skipping the call to EVP_PKEY_CTX_set1_hkdf_salt
  4. Passing nullptr as the salt argument
  5. Passing a uint8_t x[32] = {0, 0, 0, ..., 0} with length 32
  6. Passing a uint8_t x[32] = {0, 0, 0, ..., 1} with length 32

In all cases, the same key was generated and the test passes except cases 4 and 6 where the test instead fails as expected. Case 4 fails because of the potential regression mentioned earlier, and case 6 fails because the salt used is not the same as in the other cases and therefore causes the generated key to be different.

@riwoh

riwoh commented Nov 21, 2025

Copy link
Copy Markdown
Contributor

Hmm, was able to dig up this issue pointing to the potential regression: openssl/openssl#28200

I created a simple test:

const auto key_size = 16;
std::vector<uint8_t> salt{};
std::vector<uint8_t> info{};
std::vector<uint8_t> clear_key(key_size);
std::vector<uint8_t> clear_base_key{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
ASSERT_TRUE(hkdf(clear_key, clear_base_key, salt, info, SA_DIGEST_ALGORITHM_SHA256));
const std::vector<uint8_t> expected_key{0x74, 0xb2, 0, 0x5f, 0xb0, 0x20, 0xa3, 0xe0, 0x1b, 0xe2, 0xb2, 0x9d, 0xe4, 0x29, 0xb3, 0xd5};
ASSERT_EQ(clear_key, expected_key);

and modified hkdf to call EVP_PKEY_CTX_set1_hkdf_salt six different ways when the given salt is empty:

  1. Using a pointer to 0 for the salt argument with zero length
  2. Using a pointer to 1 for the salt argument with zero length
  3. Completely skipping the call to EVP_PKEY_CTX_set1_hkdf_salt
  4. Passing nullptr as the salt argument
  5. Passing a uint8_t x[32] = {0, 0, 0, ..., 0} with length 32
  6. Passing a uint8_t x[32] = {0, 0, 0, ..., 1} with length 32

In all cases, the same key was generated and the test passes except cases 4 and 6 where the test instead fails as expected. Case 4 fails because of the potential regression mentioned earlier, and case 6 fails because the salt used is not the same as in the other cases and therefore causes the generated key to be different.

@jcarrete5 do you think this is a good unit test to add (I.e. do you want to add it)? The behavior seems like it could change in a future OpenSSL version.

@riwoh riwoh changed the title Hkdf fix Bug #158: HKDF behavior mismatch when passing in nullptr due to OpenSSL changes. Nov 21, 2025
@riwoh riwoh changed the title Bug #158: HKDF behavior mismatch when passing in nullptr due to OpenSSL changes. Issue #158: HKDF behavior mismatch when passing in nullptr due to OpenSSL changes. Nov 21, 2025
@jcarrete5

Copy link
Copy Markdown
Contributor

do you think this is a good unit test to add (I.e. do you want to add it)? The behavior seems like it could change in a future OpenSSL version.

I don't think so. This is really an openssl test as I'm not calling any secapi-specific things here. I think it should be clear in the code that this is a workaround for what appears to be a regression in openssl. Preferably with a link to the GitHub issue I mentioned.

@riwoh

riwoh commented Nov 21, 2025

Copy link
Copy Markdown
Contributor

do you think this is a good unit test to add (I.e. do you want to add it)? The behavior seems like it could change in a future OpenSSL version.

I don't think so. This is really an openssl test as I'm not calling any secapi-specific things here. I think it should be clear in the code that this is a workaround for what appears to be a regression in openssl. Preferably with a link to the GitHub issue I mentioned.

I think the behavior before was incorrect, and we should probably keep the unit test updated to the latest. Previously a null would result in a 32-byte salt, whereas now it results in an error, which seems like better behavior.

@riwoh
riwoh merged commit 4811827 into rdkcentral:main Jan 16, 2026
5 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jan 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants