X509_VERIFY_CERT(3ossl)

๐Ÿ’– X509_VERIFY_CERT ๐Ÿ’–

okay bestie, pull up a chair because we're reading the 3ossl manual page for X509_VERIFY_CERT and it is a whole journey. three functions, one big vibe: building and checking X509 certificate chains. let's get into it ๐Ÿฆ‹


โœจ NAME โœจ

X509_verify_cert, X509_STORE_CTX_verify, X509_build_chain โ€” build and verify X509 certificate chain. that's the whole mission statement, no notes ๐Ÿ’…


โœจ SYNOPSIS โœจ

#include <openssl/x509_vfy.h>

int X509_verify_cert(X509_STORE_CTX *ctx);
int X509_STORE_CTX_verify(X509_STORE_CTX *ctx);
STACK_OF(X509) *X509_build_chain(X509 *target, STACK_OF(X509) *certs,
                                 X509_STORE *store, int with_self_signed,
                                 OSSL_LIB_CTX *libctx, const char *propq);

โœจ DESCRIPTION โœจ

๐ŸŒธ X509_verify_cert and X509_STORE_CTX_verify ๐ŸŒธ

so X509_verify_cert() is the main character here: it tries to build AND validate a certificate chain for the target certificate that's sitting in ctx. but bestie, the verification context (type X509_STORE_CTX) doesn't just appear out of thin air โ€” you gotta construct it first with X509_STORE_CTX_new(3) and initialise it with X509_STORE_CTX_init(3). it's giving "do your skincare routine before the makeup".

and what does that context actually carry around? literally everything:

  • the target certificate
  • the trust store
  • an optional stack of untrusted certificates that may help with chain construction
  • verification parameters like flags and a verification purpose
  • an optional verification callback
  • and, after a call, the verification outcome
๐Ÿšจ BIG ONE, no cap: a X509_STORE_CTX can be used for only one verification. if you call X509_verify_cert() a second time on the same context without reinitialising it, it fails with a negative return value, and X509_STORE_CTX_get_error(3) will then hand you X509_V_ERR_INVALID_CALL. one and done, periodt.

when the target is a certificate, the function does these steps in order. the first step that fails aborts the whole thing, except where a verification callback explicitly waives the error (peep "THE VERIFICATION CALLBACK" below ๐Ÿ‘€):

  1. Chain construction. starting from the target certificate, the verification machinery hunts for an issuer for the certificate currently chilling at the top of the chain, pulling candidates from ctx's untrusted stack and from the trust store. by default the search is untrusted-first: the untrusted stack gets examined before the trust store. setting X509_V_FLAG_TRUSTED_FIRST on ctx's verification parameters flips that order. when an untrusted-first search fails to reach a trust anchor and X509_V_FLAG_NO_ALT_CHAINS is not set, the search gets retried with progressively shorter untrusted prefixes, trying to find an alternative trusted path. the chain length is bounded by the configured depth limit (see X509_VERIFY_PARAM_set_depth(3)); blow past it and you get X509_V_ERR_CERT_CHAIN_TOO_LONG. and if more than one chain is possible? only one is taken, bestie. no choosing.

failing to build a chain all the way to a trust anchor yields an error such as X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT, X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY, X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN, X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT, or X509_V_ERR_CERT_CHAIN_TOO_LONG. ๐Ÿ˜ญ

  1. Certificate extension validation per RFC 5280, including basic constraints, key usage, and the verification purpose set via X509_VERIFY_PARAM_set_purpose(3).
  2. Security level checks against the configured authentication level, covering issuer key sizes (X509_V_ERR_CA_KEY_TOO_SMALL) and signature algorithm strength (X509_V_ERR_CA_MD_TOO_WEAK). the leaf key gets checked separately, before chain construction even begins (X509_V_ERR_EE_KEY_TOO_SMALL).
  3. Identity checks against any hostnames, email addresses or IP addresses configured on the verification parameters (X509_VERIFY_PARAM_set1_host(3) and related).
  4. Revocation checks via CRLs and, when configured, OCSP. which checks actually run is controlled by flags such as X509_V_FLAG_CRL_CHECK and X509_V_FLAG_CRL_CHECK_ALL.
  5. Signature and validity period checks on every certificate in the chain, walking from the trust anchor down to the target. and here's the tea: the signature on the chain's terminating certificate is not verified โ€” trust is taken from its presence in the trust store rather than from its signature. this applies both to a conventional self-signed trust anchor and, when X509_V_FLAG_PARTIAL_CHAIN is in effect, to a non-self-signed intermediate that got promoted to anchor status. X509_V_FLAG_CHECK_SS_SIGNATURE requests that the self-signature on a self-signed terminator be verified; it has no effect when the terminator is a non-self-signed certificate. validity periods get checked on every certificate, including the terminator.
  6. Name constraint validation per RFC 5280 section 4.2.1.10; see NAME_CONSTRAINTS_check(3) for the deets on the matching primitive and the general-name types it covers.
  7. RFC 3779 path validation of AS-number and IP-address delegation extensions, performed by default unless OpenSSL was built with no-rfc3779.
  8. Certificate policy validation, performed only when X509_V_FLAG_POLICY_CHECK is set.

several X509_V_FLAG_* values modify how these checks behave; the representative ones are named at the relevant step above, but that list is not exhaustive. the complete set of verification flags, what each one does, and the functions used to query and modify them are all documented in X509_VERIFY_PARAM_set_flags(3).

also, plot twist: applications rarely call X509_verify_cert() directly. OpenSSL invokes it internally during S/MIME and CMS verification and during the TLS handshake. it's the backstage icon doing all the work โœจ

X509_STORE_CTX_verify() behaves identically to X509_verify_cert() except for how the target certificate gets picked: if no target has been set on ctx (see X509_STORE_CTX_set_cert(3)) and the untrusted stack is nonempty, the first certificate in the untrusted stack is adopted as the target before verification begins. if a target was set explicitly, X509_STORE_CTX_verify() uses it and does not consult the untrusted stack for this purpose.


๐Ÿฆ‹ Raw public key targets ๐Ÿฆ‹

when the verification target is a raw public key rather than a certificate (set via X509_STORE_CTX_init_rpk(3)), both functions validate the raw public key instead of a certificate chain. the set of possible checks is significantly reduced: there are no extensions, names, CRLs or signatures to verify. ๐Ÿ’€

the raw public key can be authenticated only via DANE TLSA records, either locally synthesised or obtained by the application from DNS. raw public key DANE TLSA records may be added via SSL_add_expected_rpk(3) or SSL_dane_tlsa_add(3).


๐ŸŽ€ X509_build_chain ๐ŸŽ€

X509_build_chain() builds a certificate chain starting from target, using the exact same chain-construction algorithm as X509_verify_cert() (scroll up to "X509_verify_cert and X509_STORE_CTX_verify" if you skipped it, bestie). internally it uses a X509_STORE_CTX structure associated with the library context libctx and property query string propq, both of which may be NULL.

the role of certs depends on store:

  • if store is non-NULL, certs is treated as an optional list of untrusted intermediate certificates that may help complete the chain, and the chain must reach a trust anchor contained in store. if no chain to a trust anchor can be built, the function fails and returns NULL.
  • if store is NULL, certs is installed as the trusted stack for the internal context (see X509_STORE_CTX_set0_trusted_stack(3)). in this mode the function builds the chain as far as it can but does not require it to reach an anchor: if chain construction fails partway, the partial chain built so far is still returned. generous queen ๐Ÿซถ

because the internal X509_STORE_CTX is allocated and freed inside X509_build_chain(), search-policy flags such as X509_V_FLAG_TRUSTED_FIRST and X509_V_FLAG_NO_ALT_CHAINS and the configured depth limit always take their default values, and the specific X509_V_ERR_* code that caused chain construction to fail is not reported back: the function signals only success or failure through its return value (and, when store is NULL, may also return a partial chain).

on success the returned stack starts with a newly up-referenced target followed by the issuer certificates that were found. a self-signed certificate at the top of the chain is included in the returned stack when either with_self_signed is 1, or the chain consists solely of target (for example because target itself is self-signed or no further issuer could be found). when the chain has more than one element and with_self_signed is 0, the self-signed top is omitted from the result.

๐ŸŒธ friendly reminder: the caller is responsible for freeing the returned stack. don't leak, bestie, it's not the vibe.

โœจ THE VERIFICATION CALLBACK โœจ

Okay so every single X509_STORE_CTX is carrying around a verification callback, and its signature looks like this:

int (*verify_cb)(int ok, X509_STORE_CTX *ctx);

X509_verify_cert() and X509_STORE_CTX_verify() call this thing at multiple points during verification โ€” sometimes to spill the tea about errors, sometimes just to be like "hey bestie, progress โœจ". You install it on the context with X509_STORE_CTX_set_verify_cb(3), or it gets inherited at X509_STORE_CTX_init(3) time from the X509_STORE (peep X509_STORE_set_verify_cb_func(3) for that one). And if nobody set one at all? There's a default callback, and all it does is hand back its ok argument unchanged, which means every error straight up aborts verification. ๐Ÿ’€


๐ŸŒธ When the callback is called ๐ŸŒธ

There are two totally distinct invocation patterns, and you gotta know both:

Error notification (ok = 0)

Every time a check fails, the callback gets called with ok set to 0. Right before that call, the verification machinery writes the X509_V_ERR_* code describing the failure onto ctx, and for certificate-level errors it also records the depth at which the error was detected plus the certificate in question. Your callback peeks at all of that through X509_STORE_CTX_get_error(3), X509_STORE_CTX_get_error_depth(3), and X509_STORE_CTX_get_current_cert(3). Heads up tho: CRL- and OCSP-related errors only update the error code โ€” the depth and current certificate just keep whatever values they had from a preceding context. ๐Ÿ™ˆ

Per-certificate success notification (ok = 1)

During the signature-and-validity pass, once each certificate in the chain has been checked successfully, the callback gets called with ok set to 1. The current certificate, current issuer, and error depth โ€” queryable via X509_STORE_CTX_get_current_cert(3), X509_STORE_CTX_get0_current_issuer(3), and X509_STORE_CTX_get_error_depth(3) respectively โ€” describe the certificate that just got accepted. Your callback can use this to log progress if that's your vibe, but it must return a nonzero value, otherwise verification gets aborted. ๐Ÿ˜ญ

The callback's return value is what controls subsequent verification, so pay attention:

  • A nonzero return value keeps verification going. For an error notification, this is what "waiving the error" means.
  • A zero return value aborts verification immediately. The function returns 0 to its caller in that case, no matter whether the callback was invoked with ok = 0 or ok = 1.

๐Ÿ’… Sticky errors ๐Ÿ’…

When the callback waives an error by returning nonzero, the underlying check is treated as passed for control-flow purposes, but the error code recorded on ctx is not reset to X509_V_OK. So a subsequent successful return from X509_verify_cert() does not imply that X509_STORE_CTX_get_error(3) will return X509_V_OK โ€” it may still be holding the last error code that got waived. This is intentional, bestie. Only the callback itself is allowed to overwrite the error code, via X509_STORE_CTX_set_error(3), and only at its own risk. ๐Ÿซถ


๐Ÿ”ฅ Dangers ๐Ÿ”ฅ

A verification callback that returns nonzero on an error notification has, by definition, suppressed an authentication check that OpenSSL considered necessary. Callers should treat installing a callback that waives errors as a deliberate weakening of the security guarantees of X509_verify_cert(), and only ever do it for specific, well-understood error codes. Here are the pitfalls that get people every time:

  • Blanket waivers. A callback that returns 1 unconditionally turns X509_verify_cert() into "accept anything" and is almost always wrong outside of diagnostics. Inspect the error code via X509_STORE_CTX_get_error(3) and waive only the specific codes you actually intend to.
  • Failing a success notification. Because the callback is also called with ok = 1, a callback that mistakenly returns 0 in that case causes verification to fail even though every check passed. And based on the return value alone, the caller cannot tell this apart from a genuine failure. ๐Ÿ™ƒ
  • Clearing the error code. The sticky-error rule exists so that a waived error stays visible to the caller after verification returns. A callback that calls X509_STORE_CTX_set_error(3) with X509_V_OK hides that information, and can also mask a later error if the callback gets invoked again before verification completes.
  • Mutating the verification context. The X509_STORE_CTX is live during the callback โ€” the verification routines are actively reading its chain, parameters, and other state. Calling context-mutating functions from inside the callback โ€” like replacing the verified chain via X509_STORE_CTX_set0_verified_chain(3), swapping the trust store or untrusted stack, or changing verification flags, depth, purpose, or target โ€” can corrupt the in-progress verification, produce inconsistent behaviour between later steps of the pipeline, or, in the case of the verified chain, cause use-after-free. ๐Ÿ’€ The only mutators that are reasonable from within a callback are the error-related setters (X509_STORE_CTX_set_error(3), X509_STORE_CTX_set_error_depth(3), X509_STORE_CTX_set_current_cert(3)), and even those should be used sparingly (see "Sticky errors").
  • Heavy work in the callback. The callback sits on the hot path of every certificate check; expensive work performed there will slow down every single TLS handshake or S/MIME verification that uses the surrounding context. ๐Ÿ˜ฎโ€๐Ÿ’จ
  • Trusting the depth alone. The error depth, from X509_STORE_CTX_get_error_depth(3), records where an error was detected during chain processing โ€” not the position of the certificate in the final chain. Always consult X509_STORE_CTX_get_current_cert(3) alongside the depth when deciding whether to waive.
  • Fragility with respect to check order. The set of errors the callback observes, and the order in which it observes them, depends on the internal order in which verification checks are performed. When a certificate has more than one problem, only the first check to detect a problem causes the callback to be invoked for that certificate; later checks are not reached unless the callback waives the earlier error. This ordering is an implementation detail and is not part of the stable API: a refactor that reorders internal checks without altering the binary success/failure contract of X509_verify_cert() may still change which X509_V_ERR_* code the callback sees, or whether a given code is reported at all. Callbacks that branch on a specific error code being reported, or that assume earlier checks have already filtered out certain conditions, can therefore change behaviour silently across OpenSSL releases. So write your callbacks defensively: re-fetch the error code and the current certificate via X509_STORE_CTX_get_error(3) and X509_STORE_CTX_get_current_cert(3) afresh on each invocation, and treat "this error code never appears" as an assumption that may be invalidated. And more fundamentally โ€” because the set and order of error notifications is not a stable contract, the callback cannot be relied upon to observe any particular condition or sequence of conditions, which makes it an unsound mechanism for enforcing or modifying security policy. Using the verification callback to alter verification outcomes โ€” waiving errors, injecting conditional acceptance, or gating behaviour on a specific X509_V_ERR_* code being reported โ€” is therefore discouraged in production code. Reserve the callback for diagnostic and logging purposes, where future changes in which errors appear, or in what order, are not security-relevant. ๐Ÿ‘‘

The default callback waives nothing and is the safe choice; it is the right behaviour for almost all production uses. Periodt. ๐Ÿ’–


โญ RETURN VALUES โญ

X509_verify_cert() and X509_STORE_CTX_verify() return:

  • 1 โ€” if a complete chain has been built and every check either succeeded or was waived by the verification callback. Note that the latter case does not guarantee that X509_STORE_CTX_get_error(3) returns X509_V_OK; see "Sticky errors". The return value is the authoritative success/failure signal: callers do not need to additionally check that X509_STORE_CTX_get_error(3) returns X509_V_OK to consider verification successful. They may consult it to learn whether, and which, errors were waived by the verification callback. โœจ
  • 0 โ€” if verification was rejected. This happens when a check failed and the callback did not waive the error, when a trust decision actively rejected the chain, or when the verification callback returned 0 from a success notification (see "THE VERIFICATION CALLBACK"). When a certificate would have failed more than one check, the specific X509_V_ERR_* code returned by X509_STORE_CTX_get_error(3) reflects whichever check fired first; this ordering is an implementation detail and is not stable across releases. Callers must therefore treat the return value as the authoritative success/failure signal, and treat the specific error code as diagnostic information that may shift over time. ๐Ÿฆ‹
  • A negative value โ€” on a hard error that prevented verification from running to completion. The documented cases are: ctx is NULL; ctx has no target certificate set; ctx has already been used for a previous verification; memory allocation failed; the trust store lookup function returned an error; or an internal invariant was violated. In these cases X509_STORE_CTX_get_error(3) returns an appropriate X509_V_ERR_* value (X509_V_ERR_INVALID_CALL, X509_V_ERR_OUT_OF_MEM, X509_V_ERR_STORE_LOOKUP or X509_V_ERR_UNSPECIFIED). ๐Ÿ˜ญ

In all failure modes, additional information can be obtained from X509_STORE_CTX_get_error(3) and the related accessors. Applications must treat any return value <= 0 as verification not having succeeded. No cap. ๐Ÿ’…

X509_build_chain() returns NULL on error. Otherwise it returns a newly allocated stack of certificates that the caller must free; the stack may represent only a partial chain when store is NULL.

๐Ÿ’€ BUGS ๐Ÿ’€ (read this, bestie, fr fr)

Okay so this is the part where we're honest: chain construction has some quirks, and several aspects of it straight-up depart from the recommendations of RFC 4158 (Certification Path Building) and from strict RFC 5280 path validation. Callers should be aware of the following, no cap:

  • The chain search is not optimised in the manner described by RFC 4158 sections 3.1 to 3.5. Candidate issuers are not scored against the set of heuristics RFC 4158 recommends. At each step, the first viable candidate is committed to, and the only preference is for a candidate whose validity period covers the current time. There's no tree-traversal backtracking either: when an initial chain doesn't reach a trust anchor, the search gets retried with progressively shorter untrusted prefixes (unless X509_V_FLAG_NO_ALT_CHAINS is set), but different candidate issuers at intermediate positions of the same chain are never tried. In simple hierarchical PKIs this is rarely an issue, but in cross-certified or bridged PKI environments, X509_verify_cert() may fail to find a valid certification path even when one demonstrably exists in the available certificate set. ๐Ÿ˜ญ it's giving "the path was right there".
  • Issuer key usage is not enforced during chain construction. RFC 5280 section 6.1.4(n) and RFC 4158 section 3.5.3 call for verifying that an issuer candidate's keyUsage extension permits certificate signing (keyCertSign) before that candidate is selected. OpenSSL defers this check to the later extension-validation pass: if two candidate issuers exist for a certificate, and the one lacking keyCertSign happens to be selected first, verification fails on the extension check rather than backing off and trying the other candidate. The misuse is ultimately caught, but a chain that would have validated through the alternative issuer is not built. ๐Ÿ’… caught, yes โ€” gracefully handled, absolutely not.
  • X509_V_FLAG_PARTIAL_CHAIN relaxes the trust-anchor requirement from the one defined by RFC 5280 section 6.1.1(d). With the flag set, any certificate in the trust store is acceptable as the terminator of the chain, even if it is not a self-signed root. This is an intentional and now-common deviation that supports modern practices such as pinning trust to a specific intermediate, or shortening chains by treating a sufficiently-trusted intermediate as the trust point and eliding the root above it. Callers should nevertheless be aware that the chain returned in this mode does not necessarily terminate at an RFC 5280-style trust anchor. ๐Ÿ‘‘
  • NAME_CONSTRAINTS_check(3) caps each per-pair check at 2**20 comparisons, but the chain orchestrator issues N(N-1)/2 such checks for an N-certificate chain. An adversary submitting a maximally constructed chain can therefore force up to approximately 6.3 million name-constraint comparisons in a four-certificate chain (one leaf and three name-constrained signers), or approximately 5.2 billion at the default chain-depth limit of 100. ๐Ÿ”ฅ that math is not mathing in our favor.

๐ŸŒˆ SEE ALSO ๐ŸŒˆ

Go read your siblings, bestie, they're all iconic:


๐Ÿฆ‹ HISTORY ๐Ÿฆ‹

X509_build_chain() and X509_STORE_CTX_verify() were added in OpenSSL 3.0. baby steps into greatness, honestly.


Copyright 2009-2026 The OpenSSL Project Authors. All Rights Reserved.

Licensed under the Apache License 2.0 (the "License"). You may not use this file except in compliance with the License. You can obtain a copy in the file LICENSE in the source distribution or at <https://www.openssl.org/source/license.html>.


And that's a wrap on X509_VERIFY_CERT(3ossl), bestie!! โœจ This whole page ships with OpenSSL version 3.5.9 and it's dated 2026-09-29 โ€” so if you're reading a newer copy, go touch grass and check the current docs. You made it to the end of a three-part manpage, which is genuinely main character energy. ๐Ÿซถ๐Ÿ’– periodt.

๐Ÿ“ฆ where this page comes from