-
Notifications
You must be signed in to change notification settings - Fork 376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support x5c JWT header as specified in RFC-7515 (JWS) #308
Conversation
Hello, @itstehkman! This is your first Pull Request that will be reviewed by Ebert, an automatic Code Review service. It will leave comments on this diff with potential issues and style violations found in the code as you push new commits. You can also see all the issues found on this Pull Request on its review page. Please check our documentation for more information. |
lib/jwt/x509/validator.rb
Outdated
end | ||
|
||
def valid? | ||
return true if @cert_chain.nil? || @cert_chain.empty? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use @cert_chain.blank?
instead of @cert_chain.nil? || @cert_chain.empty?
.
lib/jwt/x509/validator.rb
Outdated
# @option opts [String] 'x5t' (not yet supported) | ||
def initialize(opts) | ||
unless opts['x5c'].nil? | ||
signing_der = ::Base64.decode64((opts['x5c'].first)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't use parentheses around a method call.
lib/jwt/decode.rb
Outdated
|
||
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty? | ||
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header? | ||
|
||
if @options[:validate_cert] == true && !!@header['x5c'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid the use of double negation (!!
).
lib/jwt/decode.rb
Outdated
|
||
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty? | ||
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header? | ||
|
||
if @options[:validate_cert] == true && !!@header['x5c'] | ||
return false unless ::JWT::X509::Validator.new({ 'x5c' => header['x5c'] }).valid? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant curly braces around a hash parameter.
lib/jwt/decode.rb
Outdated
@@ -35,10 +35,15 @@ def decode_segments | |||
def verify_signature | |||
@key = find_key(&@keyfinder) if @keyfinder | |||
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks] | |||
@key = ::JWT::X509::KeyFinder.new({ 'x5c' => header['x5c'] }).public_key if header['x5c'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant curly braces around a hash parameter.
@@ -0,0 +1,30 @@ | |||
module JWT | |||
module X509 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JWT::X509 has the name 'X509'
lib/jwt/x509/validator.rb
Outdated
end | ||
|
||
def valid? | ||
return true if @cert_chain.nil? || @cert_chain.empty? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JWT::X509::Validator#valid? performs a nil-check
lib/jwt/x509/validator.rb
Outdated
# @option opts [Array<String>] 'x5c' x509 cert chain, where first is the cert used to sign | ||
# @option opts [String] 'x5t' (not yet supported) | ||
def initialize(opts) | ||
unless opts['x5c'].nil? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JWT::X509::Validator#initialize performs a nil-check
lib/jwt/x509/validator.rb
Outdated
# @option opts [Array<String>] 'x5c' x509 cert chain, where first is the cert used to sign | ||
# @option opts [String] 'x5t' (not yet supported) | ||
def initialize(opts) | ||
unless opts['x5c'].nil? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JWT::X509::Validator#initialize calls 'opts['x5c']' 4 times
@@ -0,0 +1,17 @@ | |||
module JWT | |||
module X509 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JWT::X509 has the name 'X509'
# @option opts [Array<String>] 'x5c' x509 cert chain, where first is the cert used to sign | ||
# @option opts [String] 'x5t' (not yet supported) | ||
def initialize(opts) | ||
unless opts['x5c'].nil? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JWT::X509::KeyFinder#initialize performs a nil-check
lib/jwt/decode.rb
Outdated
@@ -35,10 +35,15 @@ def decode_segments | |||
def verify_signature | |||
@key = find_key(&@keyfinder) if @keyfinder | |||
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks] | |||
@key = ::JWT::X509::KeyFinder.new({ 'x5c' => header['x5c'] }).public_key if header['x5c'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JWT::Decode#verify_signature calls 'header['x5c']' 3 times
def initialize(opts) | ||
x5c = opts[:x5c] | ||
|
||
unless x5c.nil? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JWT::X509::Validator#initialize performs a nil-check
Ebert has finished reviewing this Pull Request and has found:
You can see more details about this review at https://ebertapp.io/github/jwt/ruby-jwt/pulls/308. |
signing_der = ::Base64.decode64 x5c.first | ||
@signing_cert = OpenSSL::X509::Certificate.new signing_der | ||
len = x5c.length | ||
@cert_chain = x5c[1...len].map do |b64der| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove len = x5c.length
and use:
@cert_chain = x5c[1...len].map do |b64der| | |
@cert_chain = x5c[1..-1].map do |b64der| |
@signing_cert = OpenSSL::X509::Certificate.new signing_der | ||
len = x5c.length | ||
@cert_chain = x5c[1...len].map do |b64der| | ||
OpenSSL::X509::Certificate.new(::Base64.decode64(b64der)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment above about RFC 4648.
OpenSSL::X509::Certificate.new(::Base64.decode64(b64der)) | |
OpenSSL::X509::Certificate.new(::Base64.strict_decode64(b64der)) |
x5c = opts[:x5c] | ||
|
||
unless x5c.nil? | ||
signing_der = ::Base64.decode64 x5c.first |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use the strict variant here, as documented this method complies with RFC 4648 mentioned by the JWT spec.
signing_der = ::Base64.decode64 x5c.first | |
signing_der = ::Base64.strict_decode64(x5c.first) |
store.add_cert cert | ||
end | ||
store.verify @signing_cert | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block of code has security problems. From RFC 5280 section 3.2:
In general, a chain of multiple certificates may be needed, comprising a certificate of the public key owner (the end entity) signed by one CA, and zero or more additional certificates of CAs signed by other CAs. Such chains, called certification paths, are required because a public key user is only initialized with a limited number of assured CA public keys.
More concretely:
- Chain validation can not be skipped: this would allow an attacker to just strip the chain and inject their own certificate and use it to sign, bypassing any security check. Even a single certificate must chain correctly to a trust anchor.
- The trust anchor should be configurable by the consuming app. It may be tempting to default to
OpenSSL::X509::Store#set_defaults_path
but instead of allowing all the certificate authorities to sign, the user should specify this themselves and preferably only limit to root certificate(s) that apply for their use case. E.g. when validating a JWT from Google, use only the cert(s) from https://pki.goog/ - RFC 5280 section 3.3 also mentions revocation checking, which is absent in this PR.
I've been implementing all of the above in the WebAuthn gem, you can find my code here: cedarcode/webauthn-ruby#208 - feel free to use it, I'd appreciate a mention as co-author if you do 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bdewater sorry for the late response here! Thank you for all the feedback, definitely seems like I was missing some important parts. And that webauthn PR looks impressive.
Unfortunately I am no longer working on this PR, as my project's requirements changed and I don't need to use x5c
for JWTs anymore. Please feel free to take my work and continue it if you'd like!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bdewater would you be interested in taking over this PR, because it looks like a important feature. Newer versions of the IDP's are supporting the x5c header and I think we should support it in this gem too.
Let me know if you would be able to work on this one, else I can pick it up and resolve :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm currently on vacation but was planning to pick this up again after I'm back next week :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #338
Taking a stab at supporting x5c (#59)
https://tools.ietf.org/html/rfc7515#section-4.1.6
I'll fix the ebert issues later, I just want the idea validated so I can move forward.