Skip to content

fix: include openssl/opensslv.h in src/jose.h - #1400

Merged
zandbelt merged 1 commit into
OpenIDC:masterfrom
cyrilst:master
Oct 30, 2026
Merged

zandbelt merged 1 commit into
OpenIDC:masterfrom
cyrilst:master

Conversation

@cyrilst

@cyrilst cyrilst commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Summary

After upgrading 2.4.19.1 → 2.4.20.3 (FreeBSD port, OpenSSL 3.5.6, libcurl 8.23.0), every login against an OP that signs its id_tokens with ES256 (kanidm) fails with:

[auth_openidc:error] oidc_proto_jwt_verify: JWT signature verification failed: [jose/jws.c:437: oidc_jwt_verify]: could not find key with kid: f13ecc88a99a
[auth_openidc:error] oidc_proto_idtoken_parse: id_token signature could not be validated, aborting

The JWKs URI is fetched correctly and contains the key, and the EC JWK parses fine — the failure is that oidc_jwt_alg2kty() cannot map ES256 to the EC key type in this build. With LogLevel auth_openidc:debug:

oidc_proto_idtoken_parse: enter: id_token header={"alg":"ES256","kid":"f13ecc88a99a"}
oidc_http_request: response={"keys":[{"kty":"EC","crv":"P-256","x":"…","y":"…","alg":"ES256","use":"sig","kid":"f13ecc88a99a"}]}
oidc_proto_jwks_key_apply: skipping non matching kty=2 for kid=f13ecc88a99a because it doesn't match requested kty=-1, kid=f13ecc88a99a

kty=2 is OIDC_JOSE_JWK_KTY_EC — the parsed key, correct. requested kty=-1 is oidc_alg2kty("ES256") returning -1.

Root cause

src/jose.h:73:

#define OIDC_JOSE_EC_SUPPORT OPENSSL_VERSION_NUMBER >= 0x1000100f

src/jose/jws.c:160 (inside oidc_alg2kty(), defined at jose/jws.c:151):

#if (OIDC_JOSE_EC_SUPPORT)
        if (_oidc_strncmp(alg, "ES", 2) == 0)
                return CJOSE_JWK_KTY_EC;
#endif

jose.h includes no OpenSSL header, and nothing in jose/jws.c's include chain does either (jose.h → const.h → httpd.h/http_log.h; jose/internal.h → cjose/cjose.h; <jansson.h>; util/util.h → json.h). In #if evaluation the C preprocessor replaces an undefined identifier with 0, so in that translation unit:

OIDC_JOSE_EC_SUPPORT == (0 >= 0x1000100f) == false

and the ES → EC branch is silently compiled out of oidc_alg2kty(). This is not platform-specific: no stock build of 2.4.20.x gets OPENSSL_VERSION_NUMBER defined in that TU. Minimal demonstration on any platform with a C compiler:

$ printf '#if (OPENSSL_VERSION_NUMBER >= 0x1000100f)\n#error EC_ON\n#else\n#error EC_OFF\n#endif\n' > t.c && cc -fsyntax-only t.c
t.c:4:2: error: #error EC_OFF

$ printf '#include <openssl/opensslv.h>\n#if (OPENSSL_VERSION_NUMBER >= 0x1000100f)\n#error EC_ON\n#else\n#error EC_OFF\n#endif\n' > t.c && cc -fsyntax-only t.c
t.c:3:2: error: #error EC_ON

Two aggravating factors that make this look like a key/JWKs problem during diagnosis:

  • src/jose/jwk.c does include <openssl/opensslv.h> (with bn/err/evp/pem/rsa), so EC JWK import is compiled in: the key parses (kty=2) and is then skipped by the kty pre-filter in oidc_proto_jwks_key_apply() (proto/jwks.c:88) and by the kid-path check in oidc_jwt_verify() (jose/jws.c:443). A per-TU inconsistency.
  • The startup banner (mod_auth_openidc … EC=yes) is evaluated in mod_auth_openidc.c's TU, which includes opensslv.h — it reports EC support that jose/jws.c does not actually have.

Regression introduced by

Commit 23103dde — "jose: split JWK/JWS/JWE implementations into a jose/ subdirectory" (2026-07-04, first released in 2.4.20). oidc_alg2kty() moved from src/jose.c, which includes <openssl/opensslv.h> directly, into src/jose/jws.c, which does not. jose/jwk.c kept the OpenSSL includes; jose/jws.c did not. The same setup worked on 2.4.19.1.

Impact

  • All ES* (ES256/ES384/ES512) JWT verification that relies on oidc_alg2kty() fails: id_token verification against ES-signing OPs (kanidm OAuth2 RSs configured for ES256 hit this) with "could not find key with kid" or "key type of key with kid … does not match the JWT alg header".
  • RS*/HS* are unaffected (unguarded branches in oidc_alg2kty()), presumably why this hasn't surfaced against the common RS256 OPs.
  • OIDC_JOSE_GCM_SUPPORT (jose.h:74) has the identical latent pattern; it currently only survives because the TUs that use it happen to include OpenSSL headers.

Suggested fix

Include <openssl/opensslv.h> in src/jose.h before the OIDC_JOSE_*_SUPPORT defines, so every translation unit evaluates them against the real OpenSSL version (this also covers the GCM macro):

--- a/src/jose.h
+++ b/src/jose.h
@@
 #include "json.h"
 
+/* the OIDC_JOSE_*_SUPPORT macros below evaluate OPENSSL_VERSION_NUMBER in #if
+ * directives, where the preprocessor treats an undefined identifier as 0; the
+ * OpenSSL version must therefore be known before they are defined */
+#include <openssl/opensslv.h>
+
 /* opaque forward declaration of the OpenSSL BIO type used in the oidc_jwk_pem_bio_to_jwk() prototype below */
 typedef struct bio_st BIO;

Minimal alternative: add the include at the top of src/jose/jws.c only, mirroring jose/jwk.c.

Workaround

None on the RP side (the algorithm mapping is compiled out). Either apply the one-line patch above and rebuild, or switch the OP's signing algorithm to RS256.

Environment

  • mod_auth_openidc 2.4.20.3 (FreeBSD port www/mod_auth_openidc), Apache httpd 2.4, OpenSSL 3.5.6, libcurl 8.23.0
  • OP: kanidm, OAuth2 resource server with ES256 signing (discovery: id_token_signing_alg_values_supported: ["ES256"])
  • Code flow + PKCE (S256), provider metadata via OIDCProviderMetadataURL, shm cache, OIDCSessionType server-cache

@zandbelt

Copy link
Copy Markdown
Member

Thanks @cyrilst for the report and the analysis. The diagnosis is correct, and so is where you put the fix: jose.h defines OIDC_JOSE_EC_SUPPORT / OIDC_JOSE_GCM_SUPPORT in terms of OPENSSL_VERSION_NUMBER, so jose.h should be the header that makes that macro visible.

One correction to the scope, for anyone who finds this later: not every 2.4.20.x build is affected. The bug needs both 2.4.20 or later and cjose 0.8.0 or later. Up to and including 0.6.2.x, cjose/util.h includes <openssl/rsa.h>, so jose/jws.c got OPENSSL_VERSION_NUMBER through <cjose/cjose.h>. cjose 0.8.0 removed the OpenSSL includes from its public headers (cisco/cjose@3765977). So:

  • affected: builds against cjose 0.8.x or cjose main. On FreeBSD that is every build since devel/cjose moved to a 0.8.0 snapshot on 2026-09-28.
  • not affected: builds against cjose 0.6.2.x or older. That covers the Debian/Ubuntu/Fedora/RHEL distribution packages and the packages attached to the GitHub releases.

An affected build fails all ES256/ES384/ES512 handling that goes through oidc_alg2kty(), not only id_token verification:

  • JWKS key selection, with or without a kid
  • OIDCPublicKeyFiles entries with an ES* algorithm prefix
  • signed request objects

RS*, PS* and HS* keep working.

Compiling all of src/ with -Wundef against the cjose 0.8.1 headers flags only this one spot (jose/jws.c). With this PR applied, that check comes back clean.

CI didn't catch this because it only ever built against the distribution's libcjose 0.6.2.x. The EC tests in test/test_jose.c sit behind the same macro, so they would have been compiled out against cjose 0.8 as well. We're adding a CI job that builds and tests against cjose 0.8.1, plus an unguarded oidc_alg2kty() unit test.

The fix will ship in 2.4.20.4.

@zandbelt
zandbelt merged commit 69a4ba7 into OpenIDC:master Oct 30, 2026
12 checks passed
@cyrilst

cyrilst commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Awesome, thank you!

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.

2 participants