X509: cache decoded public key, give get0_pubkey borrowed semantics - #11428
Open
julek-wolfssl wants to merge 4 commits into
Open
julek-wolfssl wants to merge 4 commits into
julek-wolfssl wants to merge 4 commits into
Conversation
…ntics wolfSSL_X509_get_pubkey() built a new WOLFSSL_EVP_PKEY, including a freshly decoded RSA or EC key, on every call, and X509_get0_pubkey() mapped to the same function. Callers following the OpenSSL get0 contract, which returns a pointer owned by the certificate, leaked the whole decoded key graph on each call. Decode the public key once into the certificate's existing WOLFSSL_X509_PUBKEY member, lazily on first use (OPENSSL_ALL already fills it at parse time), and free it with the certificate. wolfSSL_X509_get_pubkey() now returns that key with a new reference, matching X509_get_pubkey(). Add wolfSSL_X509_get0_pubkey(), which returns it without a reference, and map X509_get0_pubkey() and X509_REQ_get0_pubkey() to it. Add wolfSSL_X509_PUBKEY_get0() (X509_PUBKEY_get0). wolfSSL_X509_get_X509_PUBKEY() decodes the key too, so X509_PUBKEY_get() and X509_PUBKEY_get0_param() work outside OPENSSL_ALL. get0_param no longer dereferences a missing key and maps the stored key OID to a NID before creating the algorithm object. A certificate without a public key does not cache an empty key. The lazily decoded key is published with a compare-and-exchange, the same way the context private key cache is, so concurrent first calls on a shared certificate do not leak a key. The EC public point's internal copy is marked as set after SetECKeyExternal(), so readers of a shared key do not rebuild it. wolfSSL_X509_set_pubkey() and re-decoding a certificate drop the cached key; set_pubkey keeps it when handed that very key, and refreshes the key's algorithm OID, algorithm object and curve OID. The key.algor member is now freed in every configuration that can allocate it, not only OPENSSL_ALL. Callers that freed the result of X509_get0_pubkey() must stop; the ChangeLog records this.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved findings affect build guards, cache lifecycle, concurrent initialization, and API const correctness.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Updates X.509 public-key access to lazily cache decoded keys and match OpenSSL ownership semantics.
Changes:
- Adds cached owned and borrowed public-key accessors.
- Updates cache invalidation, cleanup, and EC key handling.
- Adds compatibility mappings, tests, documentation, and ChangeLog guidance.
File summaries
| File | Summary |
|---|---|
wolfssl/ssl.h |
Declares new public-key APIs. |
wolfssl/openssl/ssl.h |
Adds OpenSSL compatibility mappings. |
tests/api/test_ossl_x509_pk.h |
Registers the new tests. |
tests/api/test_ossl_x509_pk.c |
Tests ownership and caching behavior. |
src/x509.c |
Implements key caching and access semantics. |
src/pk_ec.c |
Preserves synchronized EC public-point state. |
src/internal.c |
Updates cache cleanup and invalidation. |
doc/dox_comments/header_files/ssl.h |
Documents the new APIs. |
ChangeLog.md |
Records the behavioral change. |
Review details
Suppressed comments (4)
src/internal.c:15264
- Moving the old cache invalidation under only
OPENSSL_EXTRA_X509_SMALLremoves it fromOPENSSL_ALL, even though theOPENSSL_ALLblock below still createsx509->key.pkey. Re-decoding or reusing an X509 then overwrites the cached pointer without releasing the old key. This guard needs to cover the cache's full set of configurations, includingOPENSSL_ALL/fullOPENSSL_EXTRA.
src/x509.c:6614 - This comment says the borrowed key is valid for the entire certificate lifetime, but
wolfSSL_X509_set_pubkey()and certificate re-decoding can free or replacex509->key.pkeybefore the certificate is freed. Document the pointer as valid only until the certificate is modified or freed, and keep the generated API documentation consistent.
/* Returns the public key of x509 without a new reference.
*
* returns a pointer to the WOLFSSL_EVP_PKEY on success and NULL on fail.
* The key is valid for the lifetime of x509 and must not be freed.
src/x509.c:6581
- When
WOLFSSL_ATOMIC_OPSis unavailable, this fallback is selected even for non-SINGLE_THREADEDbuilds and is only a plain assignment. Concurrent first calls can overwrite the cache, leak the losing decodedEVP_PKEY, and race on the cached pointer, despite the helper's concurrency guarantee. Protect this path with a mutex or use another synchronized cache mechanism.
x509->key.pkey = key;
wolfssl/ssl.h:6251
- OpenSSL declares
X509_PUBKEY_get0(const X509_PUBKEY *); this new wrapper takes a mutable pointer, so code using a constX509_PUBKEYcannot call the compatibility macro without discarding const. Make both the declaration and implementation acceptconst WOLFSSL_X509_PUBKEY*.
WOLFSSL_API WOLFSSL_EVP_PKEY* wolfSSL_X509_PUBKEY_get0(WOLFSSL_X509_PUBKEY* key);
- Files reviewed: 9/9 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+11561
to
+11562
| /* Decode the key so pkey is usable through the returned object. */ | ||
| (void)X509CachedPubKey((WOLFSSL_X509*)x509); |
| x509->key.algor = NULL; | ||
| } | ||
| #endif | ||
| #ifdef OPENSSL_EXTRA_X509_SMALL |
Comment on lines
+6572
to
+6584
| if (key != NULL) { | ||
| #ifdef WOLFSSL_ATOMIC_OPS | ||
| WOLFSSL_EVP_PKEY* current = NULL; | ||
| if (!wolfSSL_Atomic_Ptr_CompareExchange( | ||
| (void* volatile*)&x509->key.pkey, (void**)¤t, key)) { | ||
| wolfSSL_EVP_PKEY_free(key); | ||
| key = current; | ||
| } | ||
| #else | ||
| x509->key.pkey = key; | ||
| #endif | ||
| x509->key.pubKeyOID = x509->pubKeyOID; | ||
| } |
| WOLFSSL_API int wolfSSL_X509_CRL_set_nextUpdate(WOLFSSL_X509_CRL* crl, | ||
| const WOLFSSL_ASN1_TIME* time); | ||
| WOLFSSL_API WOLFSSL_EVP_PKEY* wolfSSL_X509_get_pubkey(WOLFSSL_X509* x509); | ||
| WOLFSSL_API WOLFSSL_EVP_PKEY* wolfSSL_X509_get0_pubkey(WOLFSSL_X509* x509); |
|
d2iTryMlDsaKey() left keyIdx at 0 when the size-keyed raw import path matched, so d2i_make_pkey() copied nothing and the resulting EVP PKEY carried pkey.ptr == NULL and pkey_sz == 0. Under OPENSSL_ALL the certificate parser fills x509->key.pkey through wolfSSL_d2i_PUBKEY(), and an ML-DSA certificate stores the raw public key, so the cached key had no key material. Now that wolfSSL_X509_get_pubkey() returns that cached key, wolfSSL_X509_verify() and wolfSSL_X509_REQ_verify() had nothing to verify against and failed. Raw bytes carry no length prefix, so the whole input is the key.
Set x509->key.pubKeyOID before the compare-and-exchange publishes x509->key.pkey, so a concurrent X509_PUBKEY_get0_param() never sees a key next to an OID of 0. X509_get0_pubkey() takes a const X509* in OpenSSL. Take a const WOLFSSL_X509* and cast for the lazy cache, the same way wolfSSL_X509_get_X509_PUBKEY() does.
C89 forbids mixed declarations and code. Windows (C2275) and the -Wdeclaration-after-statement Jenkins configs both rejected the declaration inside the WOLFSSL_ATOMIC_OPS block.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Previously
wolfSSL_X509_get_pubkey()(andX509_get0_pubkey(), whichmapped to it) built a brand-new
WOLFSSL_EVP_PKEYon every call,leaking the decoded key on each invocation for callers that followed
OpenSSL's get0 borrowed-pointer contract.
WOLFSSL_X509_PUBKEYmember, lazily on first use, and free it with the certificate.
wolfSSL_X509_get_pubkey()returns the cached key with a newreference, matching
X509_get_pubkey().wolfSSL_X509_get0_pubkey()(returns without a reference) and mapX509_get0_pubkey()/X509_REQ_get0_pubkey()to it.wolfSSL_X509_PUBKEY_get0()(X509_PUBKEY_get0).wolfSSL_X509_get_X509_PUBKEY()now decodes the key too, soX509_PUBKEY_get()andX509_PUBKEY_get0_param()work outsideOPENSSL_ALL.get0_paramno longer dereferences a missing key, and maps the storedkey OID to a NID before creating the algorithm object; certificates
without a public key don't cache an empty one.
context private key cache) so concurrent first calls on a shared
certificate don't leak a key; the EC public point's internal copy is
marked as set after
SetECKeyExternal()to avoid rebuilding it onshared reads.
wolfSSL_X509_set_pubkey()and re-decoding a certificate drop thecached key;
set_pubkeykeeps it when handed the same key andrefreshes the key's algorithm OID, algorithm object, and curve OID.
key.algoris now freed in every configuration that can allocate it,not only
OPENSSL_ALL.Callers that freed the result of
X509_get0_pubkey()must stop doingso; this is noted in the ChangeLog.