feat(server): support EC and EdDSA keys in OIDC JWKS validation - #2593
feat(server): support EC and EdDSA keys in OIDC JWKS validation#2593lunarwhite wants to merge 1 commit into
Conversation
Should fix before merge1. A fully-unusable JWKS wipes the working key cache and hard-fails all auth
if total > 0 && new_keys.is_empty() {
warn!(total, loaded = 0, "JWKS refresh loaded zero usable signing keys");
} else {
info!(count = new_keys.len(), "JWKS keys loaded");
}
*self.keys.write().await = new_keys; // <-- committed even when emptyThe PR detects the condition and then commits it anyway. This matters more now than before: the PR adds five new ways for a key to be dropped ( Suggested behavior: when Note that returning 2.
|
JWK kty |
crv |
Algorithm |
|---|---|---|
| RSA | — | RS256 |
For EC and OKP that is accurate. For RSA it is a limitation, not a derivation. Either fix per #2 or state plainly that RSA keys are validated as RS256 only and that RS384/RS512/PS* are not supported.
6. use and key_ops are given opposite strictness
if let Some(use_) = key.use_.as_deref().filter(|u| !u.is_empty())
&& use_ != "sig"
{
return Err(SkipReason::UseNotSig); // strict: must be exactly "sig"
}
if !key.key_ops.is_empty() && !key.key_ops.iter().any(|op| op == "sign" || op == "verify") {
return Err(SkipReason::KeyOpsNotForVerify); // lenient: sign OR verify
}These are RFC 7517's two mechanisms for expressing the same constraint, given opposite postures in adjacent lines. The gateway only ever verifies, so key_ops should require verify when present — ["sign"] on a public key is semantically incoherent, and sign is a distinct operation from verify per §4.3.
To be explicit about severity: this is a correctness and consistency fix, not a security fix. §4.3 lists sign with verify as an expressly permitted pairing, so accepting ["sign"] does not create the key-reuse exposure the RFC warns about. The combination that does — an encryption key used for verification — is already blocked, and key_ops_encrypt_only_skipped covers it.
The stronger case is one the current tests actively lock in the wrong way. key_ops_sign_and_encrypt_accepted asserts that ["sign", "encrypt"] is accepted, and that is precisely the pairing §4.3 rules out:
Multiple unrelated key operations SHOULD NOT be specified for a key because of the potential vulnerabilities associated with using the same key with multiple algorithms. Thus, the combinations "sign" with "verify", "encrypt" with "decrypt", and "wrapKey" with "unwrapKey" are permitted, but other combinations SHOULD NOT be used.
Requiring verify fixes both at once. Three test outcomes change, not just the sign-only one:
| Test | key_ops |
Now | After |
|---|---|---|---|
key_ops_verify_and_sign_accepted |
["verify"] |
accept | accept |
key_ops_verify_and_sign_accepted |
["sign"] |
accept | reject |
key_ops_sign_and_encrypt_accepted |
["sign", "encrypt"] |
accept | reject |
key_ops_encrypt_only_skipped |
["encrypt"] |
reject | reject |
Interop risk is low. Neither Google nor Microsoft Entra publishes a key_ops field at all — both rely on use: "sig" — so absent key_ops stays accepted and the mainstream providers are unaffected.
Nits
validation.algorithms = vec![cached_algorithm];is redundant —Validation::new(alg)already setsalgorithms: vec![alg].rsa_test_keygenerates a fresh 2048-bit key on each of its ~12 call sites; the oidc tests take ~7s almost entirely on keygen. A static PEM constant would be faster and deterministic.ec_coords_from_spkilocates the public point by scanning backwards for a0x04byte. It works for the fixtures but will fail confusingly if a coordinate happens to contain0x04at the wrong offset. Sincercgen'sKeyPairis already in dev-deps, consider slicing the point at its known SPKI offset instead of searching.parse_jwk's first check (use == "enc") is subsumed by the second (use != "sig"); it exists only to produce a distinct log line. Fine, but a comment saying so would save the next reader a double-take. If Security feedback: SSH hardening and policy validation #7 lands,SkipReason::KeyOpsNotForVerifygets the same treatment for free.
Signed-off-by: Yuedong Wu <dwcn22@outlook.com>
|
@mrunalp Thanks for your review. I've incorporated all of them into the latest commit. I hardcoded RSA algo to RS256 only because I thought modifying it would expand the original request's scope (RSA vs EC, EdDSA). Now the full support has been added via |
Summary
OIDC JWKS validation was RSA-only, so issuers that publish ES256/ES384 or EdDSA (Ed25519) signing keys (notably Okta ES256) failed with unknown signing key. This PR derives the algorithm for EC/OKP keys from each JWK's
kty/crv, selects the RSA algorithm from the JWK's declaredalg(defaulting to RS256), pins JWT headeralgagainst the cached algorithm, requires standard claims, and documents the supported algorithms.Related Issue
Closes #2196
Changes
alg(RS256/RS384/RS512/PS256/PS384/PS512), defaulting to RS256 when absent; reject genuinely contradictory declarationsalgmismatchesiss,aud,exp, andsubon OIDC access tokens (aligns with workspace membership keyed bysub)use: enc,algmismatch,key_opswithoutverify) and poison duplicatekids with conflicting algorithmsdocs/reference/gateway-auth.mdx,docs/kubernetes/access-control.mdx, andarchitecture/gateway.mdTesting
mise run pre-commitpassesChecklist