Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions lib/linzer/jws.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require "jwt"
require "jwt/eddsa"
require "ed25519"
require "digest"
require "base64"

module Linzer
# JSON Web Signature (JWS) compatible key support.
Expand Down Expand Up @@ -101,6 +103,40 @@ def verify(signature, data)
algo.verify(data: data, signature: signature, verification_key: verify_key)
end

# Computes the RFC 7638 JWK SHA-256 Thumbprint for this key's public
# material.
#
# This is computed directly from the exported JWK rather than
# delegating to the underlying jwt-eddsa gem's own thumbprint/kid
# generation: jwt-eddsa (<= 0.9.0) has a bug where its OKP JWK class
# computes that value over the wrong members (an RSA-shaped {kty, n,
# x} instead of the RFC 8037-correct {crv, kty, x}), which silently
# produces a keyid that a spec-compliant verifier will reject.
#
# @return [String] base64url-encoded (no padding) SHA-256 thumbprint
# @raise [Error] if this key's JWK "kty" is not supported
#
# @see https://www.rfc-editor.org/rfc/rfc7638 RFC 7638 - JSON Web Key (JWK) Thumbprint
# @see https://www.rfc-editor.org/rfc/rfc8037 RFC 8037 - EdDSA for JWS/JWK
def jwk_thumbprint
# XXX: drop this method custom implementation and just
# return material.key_digest
# once https://github.com/jwt/ruby-jwt-eddsa/pull/26 is resolved
#
exported = material.export

members =
case exported[:kty]
when "OKP"
{crv: exported[:crv], kty: exported[:kty], x: exported[:x]}
else
raise Error, "Unsupported JWK kty for thumbprint: #{exported[:kty]}"
end

digest = Digest::SHA256.digest(JWT::JSON.generate(members))
Base64.urlsafe_encode64(digest, padding: false)
end

private

# @return [Boolean] true if this key can verify signatures
Expand Down
2 changes: 1 addition & 1 deletion lib/linzer/signature/profile/web_bot_auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def set_params!(key, components, params)
#
params[:expires] ||= Time.now.to_i + 3600
params[:tag] ||= "web-bot-auth"
params[:keyid] ||= key.material.key_digest
params[:keyid] ||= key.jwk_thumbprint
end

# Injects and signs the Signature-Agent header.
Expand Down
22 changes: 22 additions & 0 deletions spec/jws_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,28 @@
end
end

RSpec.describe Linzer::JWS::Key do
describe "#jwk_thumbprint" do
# Test vector from RFC 8037 Appendix A.3 (JWK Thumbprint Canonicalization):
# https://www.rfc-editor.org/rfc/rfc8037#appendix-A.3
#
# Computed directly here (not delegated to jwt-eddsa's own key_digest)
# because jwt-eddsa (<= 0.9.0) computes that value over the wrong JWK
# members for OKP keys; see Linzer::JWS::Key#jwk_thumbprint docs.
let(:jwk) { {kty: "OKP", crv: "Ed25519", x: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"} }
let(:key) { Linzer::JWS.jwk_import(jwk) }

it "matches the RFC 7638 JWK thumbprint of the crv/kty/x members" do
expect(key.jwk_thumbprint).to eq("kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k")
end

it "raises for unsupported JWK key types" do
rsa_key = Linzer::JWS.jwk_import(JWT::JWK.new(OpenSSL::PKey::RSA.new(2048)))
expect { rsa_key.jwk_thumbprint }.to raise_error(Linzer::Error, /Unsupported JWK kty/)
end
end
end

RSpec.describe Linzer::Signer do
context "with JWS EdDSA algorithm" do
let(:request) do
Expand Down
2 changes: 1 addition & 1 deletion spec/signature/profile/web_bot_auth_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
apply_profile

expect(params[:tag]).to eq("web-bot-auth")
expect(params[:keyid]).to eq(key.material.key_digest)
expect(params[:keyid]).to eq(key.jwk_thumbprint)
expect(params[:expires]).to eq(Time.now.to_i + 3600)
end

Expand Down
2 changes: 1 addition & 1 deletion spec/web_bot_auth_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
expect(signature.parameters).to include("created")
expect(signature.parameters).to include("expires" => be > Time.now.utc.to_i)
expect(signature.parameters["tag"]).to eq("web-bot-auth")
expect(signature.parameters["keyid"]).to eq(key.material.key_digest)
expect(signature.parameters["keyid"]).to eq(key.jwk_thumbprint)
expect(signature.parameters).to have_key("nonce")
expect(signature.metadata).to include('"@authority"').or include('"@target-uri"')
expect(headers["signature-agent"]).to eq("my-sig=\"https://example.com/someagent\"")
Expand Down