Skip to content

fix: check EVP_PKEY_get_bn_param() return values in _get_key_parameters()#159

Merged
atoomic merged 2 commits into
cpan-authors:mainfrom
atoomic:koan.atoomic/fix-issue-155
Apr 4, 2026
Merged

fix: check EVP_PKEY_get_bn_param() return values in _get_key_parameters()#159
atoomic merged 2 commits into
cpan-authors:mainfrom
atoomic:koan.atoomic/fix-issue-155

Conversation

@Koan-Bot
Copy link
Copy Markdown
Contributor

@Koan-Bot Koan-Bot commented Apr 3, 2026

Summary

On the OpenSSL 3.x path, _get_key_parameters() silently discarded the return values of all eight EVP_PKEY_get_bn_param() calls. A failure left the BIGNUM* pointer NULL, causing cor_bn2sv(NULL) to return &PL_sv_undef — making a broken or truncated key indistinguishable from a valid public key to callers of get_key_parameters().

Fixes #155

Changes

  • n and e (mandatory in every RSA key): croak via croakSsl() if either EVP_PKEY_get_bn_param() call fails, freeing any already-allocated BIGNUMs before the longjmp.
  • d, p, q, dmp1, dmq1, iqmp (absent for public keys): only croak when the call returns an error and the key is marked private via _is_private() — a NULL result is legitimate for a public key.

Test plan

  • New t/get_key_parameters.t (10 tests):
    • Private key: all eight parameters are defined, no croak
    • Public key: n and e are defined, private components are undef, no croak
  • Full test suite: 487 tests pass

Generated by Kōan /fix


Quality Report

Changes: 2 files changed, 79 insertions(+), 8 deletions(-)

Code scan: clean

Tests: passed (OK)

Branch hygiene: clean

Generated by Kōan post-mission quality pipeline

@timlegge
Copy link
Copy Markdown
Member

timlegge commented Apr 3, 2026

@Koan-Bot Review

this fails. EVP_PKEY_get_bn_param returns 1 on success and 0 on an error. an error could simply be that the parameter is not included in the key.

In openssl < 3 EVP_PKEY_get_bn_param does not fail the test. In openssl 3+ we would need to allow 0 as a valid response and set the value to undef. It probably should not croak but return an undef

@Koan-Bot
Copy link
Copy Markdown
Contributor Author

Koan-Bot commented Apr 3, 2026

PR Review — fix: check EVP_PKEY_get_bn_param() return values in _get_key_parameters()

The PR correctly identifies a real gap (unchecked return values on 3.x), but the fix is too aggressive: croaking on missing private components changes the established Perl-level API contract where public keys return undef for private components. As timlegge noted, EVP_PKEY_get_bn_param returning 0 for absent parameters is normal, not an error. The recommended approach: leave d/p/q/dmp1/dmq1/iqmp unchecked (NULL → undef via cor_bn2sv, matching pre-3.x behavior), optionally add n/e checks with goto err cleanup, and drain the error queue after expected public-key failures. The cascading inline cleanup and repeated _is_private() calls add complexity without benefit.


🔴 Blocking

1. Croaking on missing params contradicts pre-3.x behavior (`RSA.xs`, L877-912)

timlegge's review is correct. On pre-3.x paths (RSA_get0_key, RSA_get0_factors, RSA_get0_crt_params), missing private components simply return NULL pointers — no error, no croak. cor_bn2sv(NULL) returns &PL_sv_undef, which is the documented Perl-level contract: public keys return undef for private components.

The current approach of croaking when EVP_PKEY_get_bn_param returns 0 changes this contract on 3.x and makes the 3.x path behave differently from pre-3.x. A simpler, behavior-preserving fix: just ignore the return value for d/p/q/dmp1/dmq1/iqmp (they stay NULL → undef), and only check the return value for n and e.

Even for n and e, consider whether croaking is the right behavior — the pre-3.x paths don't croak either (they'd just push NULL through cor_bn2sv). If you do want to add error checking for n/e, that's a behavior change that should be consistent across all OpenSSL version paths, not just 3.x.

🟡 Important

1. _is_private() called 6 times redundantly on 3.x (`RSA.xs`, L883)

Each _is_private() call on OpenSSL 3.x performs EVP_PKEY_get_bn_param(rsa, OSSL_PKEY_PARAM_RSA_D, &d) + BN_clear_free(d). That's 6 extra EVP_PKEY_get_bn_param round-trips. If you keep this approach (which I don't recommend — see critical comment), cache the result:

int is_priv = _is_private(p_rsa);

and reuse is_priv in each check.

if (!EVP_PKEY_get_bn_param(rsa, OSSL_PKEY_PARAM_RSA_D, &d) && _is_private(p_rsa)) {
2. Cascading cleanup lines are fragile and unmaintainable (`RSA.xs`, L895-908)

Each successive error path adds one more BN_free/BN_clear_free call, reaching 7 calls on the last check. This is the exact anti-pattern that CLAUDE.md warns about — use THROW/goto err with centralized cleanup instead. The existing post-XPUSHs cleanup block (lines 894–906 in the original) already frees all 8 BIGNUMs; a goto err label before it would deduplicate all this.

However, this is moot if you adopt the simpler approach of not croaking (see critical comment).

BN_free(n); BN_free(e); BN_clear_free(d); BN_clear_free(p); BN_clear_free(q); BN_clear_free(dmp1); BN_clear_free(dmq1);
3. OpenSSL error queue not drained on skipped public-key failures (`RSA.xs`, L883)

When EVP_PKEY_get_bn_param returns 0 for a missing private component on a public key, it may push an error onto the OpenSSL error queue. The && _is_private() check skips the croak but doesn't call ERR_clear_error(). These stale errors could leak into the next croakSsl() call, producing a misleading error message for an unrelated failure.

if (!EVP_PKEY_get_bn_param(rsa, OSSL_PKEY_PARAM_RSA_D, &d) && _is_private(p_rsa)) {

🟢 Suggestions

1. Tests are good but don't cover the error path (`t/get_key_parameters.t`, L1-46)

The tests verify the happy paths (private key returns 8 defined values, public key returns 2 defined + 6 undef). Consider adding a test that exercises the error path — e.g., a corrupted or minimal key where n/e extraction might fail — to validate whatever error handling strategy is adopted. Also consider testing with Crypt::OpenSSL::Bignum loaded to verify the cor_bn2sv path that returns actual Bignum objects.


Checklist


Summary

The PR correctly identifies a real gap (unchecked return values on 3.x), but the fix is too aggressive: croaking on missing private components changes the established Perl-level API contract where public keys return undef for private components. As timlegge noted, EVP_PKEY_get_bn_param returning 0 for absent parameters is normal, not an error. The recommended approach: leave d/p/q/dmp1/dmq1/iqmp unchecked (NULL → undef via cor_bn2sv, matching pre-3.x behavior), optionally add n/e checks with goto err cleanup, and drain the error queue after expected public-key failures. The cascading inline cleanup and repeated _is_private() calls add complexity without benefit.


Automated review by Kōan

@Koan-Bot
Copy link
Copy Markdown
Contributor Author

Koan-Bot commented Apr 3, 2026

@timlegge: @Koan-Bot Review

Agree with this assessment. The pre-3.x paths (RSA_get0_key, RSA_get0_factors, RSA_get0_crt_params) never croak on missing private components — they just return NULL, which cor_bn2sv() converts to &PL_sv_undef. That's the established API contract.

On 3.x, EVP_PKEY_get_bn_param returning 0 for absent private parameters on a public key is expected behavior, not an error. The simplest correct fix is:

  1. For n and e (mandatory): optionally check the return value (these should never fail on a valid key), but this would be a new behavior not present in pre-3.x paths either.
  2. For d/p/q/dmp1/dmq1/iqmp: leave them unchecked. If the call fails, the pointer stays NULL (initialized at declaration), cor_bn2sv(NULL) returns undef, and the existing cleanup block safely calls BN_clear_free(NULL) (which is a no-op). This matches the pre-3.x behavior exactly.
  3. Drain the error queue after the calls with ERR_clear_error() if any failed, to prevent stale errors from leaking into subsequent croakSsl() calls.

The current _is_private() guard is also problematic: it's called 6 times, and each invocation on 3.x does its own EVP_PKEY_get_bn_param + BN_clear_free round-trip for the d parameter.

@timlegge
Copy link
Copy Markdown
Member

timlegge commented Apr 3, 2026

@Koan-Bot lets keep the contracted API and return the undef for any parameter that cannot be retrieved - that is, do not croak

Koan-Bot added a commit to atoomic/Crypt-OpenSSL-RSA that referenced this pull request Apr 3, 2026
EVP_PKEY_get_bn_param() pushes errors onto the OpenSSL error queue
when a parameter is absent (e.g. private components on a public key).
These stale errors could leak into the next croakSsl() call from an
unrelated operation.

Add ERR_clear_error() after the parameter extraction calls.  The API
contract is preserved: missing parameters return undef (via
cor_bn2sv(NULL)), matching the pre-3.x behavior exactly.

Add t/get_key_parameters.t to codify this contract: private keys
return 8 defined values, public keys return n/e defined + 6 undef.

Addresses review feedback on PR cpan-authors#159.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@atoomic
Copy link
Copy Markdown
Collaborator

atoomic commented Apr 3, 2026

@Koan-Bot rebase

Koan-Bot and others added 2 commits April 3, 2026 17:30
…rs()

On the OpenSSL 3.x path, the eight EVP_PKEY_get_bn_param() calls had
their return values discarded.  A failure left the BIGNUM* NULL so
cor_bn2sv(NULL) silently returned &PL_sv_undef, making a broken or
truncated key look like a public key.

- n and e are mandatory for every RSA key: croak (via croakSsl) if
  either call fails, freeing any already-allocated BIGNUMs first.
- d, p, q, dmp1, dmq1, iqmp are absent for public keys (return 0,
  leave pointer NULL), so only croak when the call returns an error
  and the key is marked private.

Add t/get_key_parameters.t to verify: private key returns all eight
params defined; public key returns n/e defined and private components
as undef; neither case croaks.

Fixes cpan-authors#155

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* (`RSA.xs`, d/p/q/dmp1/dmq1/iqmp): `EVP_PKEY_get_bn_param()` returning 0 for absent parameters is expected behavior for public keys, not an error. The return values are now intentionally ignored — NULL pointers flow through `cor_bn2sv()` as `undef`, matching the pre-3.x behavior where `RSA_get0_key`/`RSA_get0_factors`/`RSA_get0_crt_params` simply set NULL for missing fields.
- **Removed cascading inline cleanup and redundant `_is_private()` calls**: The fragile per-line cleanup chains and 6 redundant `_is_private()` calls (each performing an extra `EVP_PKEY_get_bn_param` round-trip) are eliminated.
- **Added `ERR_clear_error()` after private component fetches**: Failed `EVP_PKEY_get_bn_param()` calls on public keys may push errors onto the OpenSSL error queue; draining it prevents stale errors from leaking into subsequent operations.
- **Kept n/e mandatory checks**: `n` and `e` are present in every valid RSA key, so the croak-on-failure check is retained for these two parameters only.
@Koan-Bot
Copy link
Copy Markdown
Contributor Author

Koan-Bot commented Apr 3, 2026

Rebase: fix: check EVP_PKEY_get_bn_param() return values in _get_key_parameters()

Branch koan.atoomic/fix-issue-155 rebased onto main and force-pushed.

Diff: 2 files changed, 59 insertions(+), 2 deletions(-)

Review feedback was analyzed and applied.

Changes

  • (RSA.xs, d/p/q/dmp1/dmq1/iqmp): EVP_PKEY_get_bn_param() returning 0 for absent parameters is expected behavior for public keys, not an error. The return values are now intentionally ignored — NULL pointers flow through cor_bn2sv() as undef, matching the pre-3.x behavior where RSA_get0_key/RSA_get0_factors/RSA_get0_crt_params simply set NULL for missing fields.
  • Removed cascading inline cleanup and redundant _is_private() calls: The fragile per-line cleanup chains and 6 redundant _is_private() calls (each performing an extra EVP_PKEY_get_bn_param round-trip) are eliminated.
  • Added ERR_clear_error() after private component fetches: Failed EVP_PKEY_get_bn_param() calls on public keys may push errors onto the OpenSSL error queue; draining it prevents stale errors from leaking into subsequent operations.
  • Kept n/e mandatory checks: n and e are present in every valid RSA key, so the croak-on-failure check is retained for these two parameters only.

Actions

  • Already-solved check: negative (confidence=high, reasoning=The current main branch still has unchecked EVP_PKEY_get_bn_param() return values in _get_key_parame)
  • Rebased koan.atoomic/fix-issue-155 onto upstream/main
  • Applied review feedback
  • Force-pushed koan.atoomic/fix-issue-155 to origin
  • CI check enqueued (async)

CI

CI will be checked asynchronously.


Automated by Kōan

@Koan-Bot Koan-Bot force-pushed the koan.atoomic/fix-issue-155 branch from f70e835 to b4ec4ff Compare April 3, 2026 23:31
@timlegge timlegge marked this pull request as ready for review April 4, 2026 23:22
Copy link
Copy Markdown
Member

@timlegge timlegge left a comment

Choose a reason for hiding this comment

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

Approved

@atoomic atoomic merged commit f3b02be into cpan-authors:main Apr 4, 2026
28 checks passed
Koan-Bot added a commit to atoomic/Crypt-OpenSSL-RSA that referenced this pull request Apr 5, 2026
EVP_PKEY_get_bn_param() pushes errors onto the OpenSSL error queue
when a parameter is absent (e.g. private components on a public key).
These stale errors could leak into the next croakSsl() call from an
unrelated operation.

Add ERR_clear_error() after the parameter extraction calls.  The API
contract is preserved: missing parameters return undef (via
cor_bn2sv(NULL)), matching the pre-3.x behavior exactly.

Add t/get_key_parameters.t to codify this contract: private keys
return 8 defined values, public keys return n/e defined + 6 undef.

Addresses review feedback on PR cpan-authors#159.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

robustness: _get_key_parameters() silently swallows EVP_PKEY_get_bn_param() errors on OpenSSL 3.x

3 participants