From 8d3d8f67a4ed9ff75b4b2222ee9fe2f300fcd80c Mon Sep 17 00:00:00 2001 From: Samuel Cochran Date: Tue, 29 Sep 2026 04:39:18 +0000 Subject: [PATCH 1/2] fix: reject unsupported OKP curves Amp-Thread-ID: https://ampcode.com/threads/T-01a0eb70-d1e6-71dd-b774-7a79333b11f8 Co-authored-by: Amp --- README.md | 3 +++ lib/jwt/eddsa/jwk/okp.rb | 13 ++++++++++--- spec/integration_spec.rb | 8 ++++++++ spec/jwt/jwk/okp_spec.rb | 21 +++++++++++++++++++++ 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2d62b3f..87d9db1 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ token = JWT.encode({pay: "load"}, private_key, "EdDSA") payload, header = JWT.decode(token, private_key.verify_key, true, algorithm: "EdDSA") ``` +OKP JWKs must specify `crv: "Ed25519"`. Importing a JWK with a missing or +unsupported curve (including `X25519` and `Ed448`) raises `JWT::JWKError`. + ## Development ``` diff --git a/lib/jwt/eddsa/jwk/okp.rb b/lib/jwt/eddsa/jwk/okp.rb index 35af1ae..134bc55 100644 --- a/lib/jwt/eddsa/jwk/okp.rb +++ b/lib/jwt/eddsa/jwk/okp.rb @@ -86,11 +86,18 @@ def extract_key_params(key) # rubocop:disable Metrics/MethodLength end end - def check_jwk_params!(key_params, _given_params) - return if key_params[:kty] == KTY + def check_jwk_params!(key_params, given_params) + key_params = key_params.merge(given_params) + + unless key_params[:kty] == KTY + raise JWT::JWKError, + "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" + end + + return if key_params[:crv] == "Ed25519" raise JWT::JWKError, - "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" + "Incorrect 'crv' value: #{key_params[:crv]}, expected Ed25519" end def parse_okp_key_params(verify_key, signing_key = nil) diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index 53d47b2..c25f12a 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -68,6 +68,14 @@ { algorithms: ["EDDSA"], jwks: key_loader }) expect(payload).to eq(token_payload) end + + it "rejects an Ed25519 key labelled as X25519" do + public_jwks[:keys].first[:crv] = "X25519" + + expect do + JWT.decode(signed_token, nil, true, algorithms: ["EdDSA"], jwks: public_jwks) + end.to raise_error(JWT::JWKError, /Incorrect 'crv' value/) + end end describe "JWK as key" do diff --git a/spec/jwt/jwk/okp_spec.rb b/spec/jwt/jwk/okp_spec.rb index be2adb7..fee498b 100644 --- a/spec/jwt/jwk/okp_spec.rb +++ b/spec/jwt/jwk/okp_spec.rb @@ -38,6 +38,27 @@ it { is_expected.to be_a(described_class) } end + ["X25519", "Ed448", "ed25519", nil].each do |curve| + context "when the curve is #{curve.inspect}" do + let(:key) { described_class.new(public_key).export.merge(crv: curve) } + + it "rejects the JWK" do + expect { instance }.to raise_error(JWT::JWKError, /Incorrect 'crv' value/) + end + + it "rejects a curve override on a native key" do + expect { described_class.new(private_key, "crv" => curve) } + .to raise_error(JWT::JWKError, /Incorrect 'crv' value/) + end + end + end + + it "rejects a JWK without the required curve" do + parameters = described_class.new(public_key).export + parameters.delete(:crv) + expect { described_class.import(parameters) }.to raise_error(JWT::JWKError, /Incorrect 'crv' value/) + end + context "when a random key found from the Internet is given" do let(:key) do { From 46ce314e8835ed1fa69c12933ad15dd2cb441ce7 Mon Sep 17 00:00:00 2001 From: Samuel Cochran Date: Fri, 9 Oct 2026 05:24:33 +0000 Subject: [PATCH 2/2] fix: defer OKP curve checks until key access Amp-Thread-ID: https://ampcode.com/threads/T-01a0eb70-d1e6-71dd-b774-7a79333b11f8 Co-authored-by: Amp --- README.md | 6 +++-- lib/jwt/eddsa/jwk/okp.rb | 18 ++++++++------- spec/integration_spec.rb | 24 ++++++++++++++++++++ spec/jwt/jwk/okp_spec.rb | 49 +++++++++++++++++++++++----------------- 4 files changed, 66 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 87d9db1..05f9e75 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,10 @@ token = JWT.encode({pay: "load"}, private_key, "EdDSA") payload, header = JWT.decode(token, private_key.verify_key, true, algorithm: "EdDSA") ``` -OKP JWKs must specify `crv: "Ed25519"`. Importing a JWK with a missing or -unsupported curve (including `X25519` and `Ed448`) raises `JWT::JWKError`. +OKP JWKs must specify `crv: "Ed25519"` to provide a signing or verification key. +Requesting a key with a missing or unsupported curve (including `X25519` and +`Ed448`) raises `JWT::JWKError`. Such JWKs can still be imported, so an unrelated +key in a mixed JWKS does not prevent verification with a selected Ed25519 key. ## Development diff --git a/lib/jwt/eddsa/jwk/okp.rb b/lib/jwt/eddsa/jwk/okp.rb index 134bc55..22860a9 100644 --- a/lib/jwt/eddsa/jwk/okp.rb +++ b/lib/jwt/eddsa/jwk/okp.rb @@ -23,12 +23,14 @@ def initialize(key, params = nil, options = {}) end def verify_key + check_curve! return @verify_key if defined?(@verify_key) @verify_key = verify_key_from_parameters end def signing_key + check_curve! return @signing_key if defined?(@signing_key) @signing_key = signing_key_from_parameters @@ -86,18 +88,18 @@ def extract_key_params(key) # rubocop:disable Metrics/MethodLength end end - def check_jwk_params!(key_params, given_params) - key_params = key_params.merge(given_params) + def check_jwk_params!(key_params, _given_params) + return if key_params[:kty] == KTY - unless key_params[:kty] == KTY - raise JWT::JWKError, - "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" - end + raise JWT::JWKError, + "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" + end - return if key_params[:crv] == "Ed25519" + def check_curve! + return if self[:crv] == "Ed25519" raise JWT::JWKError, - "Incorrect 'crv' value: #{key_params[:crv]}, expected Ed25519" + "Incorrect 'crv' value: #{self[:crv]}, expected Ed25519" end def parse_okp_key_params(verify_key, signing_key = nil) diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index c25f12a..75e2155 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -76,6 +76,30 @@ JWT.decode(signed_token, nil, true, algorithms: ["EdDSA"], jwks: public_jwks) end.to raise_error(JWT::JWKError, /Incorrect 'crv' value/) end + + context "with a mixed JWKS" do + let(:public_jwks) do + { keys: [ + { kty: "OKP", crv: "X25519", use: "enc", kid: "encryption", + x: Base64.urlsafe_encode64("x" * 32, padding: false) }, + { kty: "OKP", crv: "Ed448", kid: "ed448", x: Base64.urlsafe_encode64("x" * 57, padding: false) }, + jwk.export + ] } + end + + it "verifies with the selected Ed25519 key" do + payload, _header = JWT.decode(signed_token, nil, true, algorithms: ["EdDSA"], jwks: public_jwks) + expect(payload).to eq(token_payload) + end + + it "rejects selecting an unsupported curve for EdDSA" do + token = JWT.encode(token_payload, jwk.signing_key, "EdDSA", kid: "ed448") + + expect do + JWT.decode(token, nil, true, algorithms: ["EdDSA"], jwks: public_jwks) + end.to raise_error(JWT::JWKError, /Incorrect 'crv' value/) + end + end end describe "JWK as key" do diff --git a/spec/jwt/jwk/okp_spec.rb b/spec/jwt/jwk/okp_spec.rb index fee498b..617b4f1 100644 --- a/spec/jwt/jwk/okp_spec.rb +++ b/spec/jwt/jwk/okp_spec.rb @@ -38,27 +38,6 @@ it { is_expected.to be_a(described_class) } end - ["X25519", "Ed448", "ed25519", nil].each do |curve| - context "when the curve is #{curve.inspect}" do - let(:key) { described_class.new(public_key).export.merge(crv: curve) } - - it "rejects the JWK" do - expect { instance }.to raise_error(JWT::JWKError, /Incorrect 'crv' value/) - end - - it "rejects a curve override on a native key" do - expect { described_class.new(private_key, "crv" => curve) } - .to raise_error(JWT::JWKError, /Incorrect 'crv' value/) - end - end - end - - it "rejects a JWK without the required curve" do - parameters = described_class.new(public_key).export - parameters.delete(:crv) - expect { described_class.import(parameters) }.to raise_error(JWT::JWKError, /Incorrect 'crv' value/) - end - context "when a random key found from the Internet is given" do let(:key) do { @@ -74,6 +53,34 @@ end end + describe "key access with an unsupported curve" do + ["X25519", "Ed448", "ed25519", nil].each do |curve| + context "when the curve is #{curve.inspect}" do + let(:key) { described_class.new(private_key).export(include_private: true).merge(crv: curve) } + + it "imports the JWK but rejects signing and verification key access" do + jwk = instance + expect { jwk.verify_key }.to raise_error(JWT::JWKError, /Incorrect 'crv' value/) + expect { jwk.signing_key }.to raise_error(JWT::JWKError, /Incorrect 'crv' value/) + end + + it "rejects returning a native key with a curve override" do + jwk = described_class.new(private_key, "crv" => curve) + expect { jwk.verify_key }.to raise_error(JWT::JWKError, /Incorrect 'crv' value/) + expect { jwk.signing_key }.to raise_error(JWT::JWKError, /Incorrect 'crv' value/) + end + end + end + + it "rejects key access without the required curve" do + parameters = described_class.new(private_key).export(include_private: true) + parameters.delete(:crv) + jwk = described_class.import(parameters) + expect { jwk.verify_key }.to raise_error(JWT::JWKError, /Incorrect 'crv' value/) + expect { jwk.signing_key }.to raise_error(JWT::JWKError, /Incorrect 'crv' value/) + end + end + describe "#verify_key" do let(:key) { private_key } subject { instance.verify_key }