Created
July 11, 2025 16:21
-
-
Save joshuaflanagan/640ef8ef86769c6f2a1c4d0645079e03 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'base64' | |
require 'json' | |
require 'openssl' | |
require 'optparse' | |
def decode_base64url(str) | |
# Add padding if necessary | |
str += '=' * (4 - str.length % 4) if str.length % 4 != 0 | |
Base64.urlsafe_decode64(str) | |
end | |
def convert_ecdsa_signature_to_der(signature, key_size) | |
# ECDSA signatures in JWT are in IEEE P1363 format (r || s) | |
# OpenSSL expects DER format, so we need to convert | |
# Determine coordinate size based on key size | |
coord_size = (key_size + 7) / 8 # Round up to nearest byte | |
# Split signature into r and s components | |
return nil if signature.length != coord_size * 2 | |
r = signature[0, coord_size] | |
s = signature[coord_size, coord_size] | |
# Convert to integers | |
r_int = r.unpack1('H*').to_i(16) | |
s_int = s.unpack1('H*').to_i(16) | |
# Create DER-encoded signature | |
OpenSSL::ASN1::Sequence.new([ | |
OpenSSL::ASN1::Integer.new(r_int), | |
OpenSSL::ASN1::Integer.new(s_int) | |
]).to_der | |
end | |
def verify_signature(header, payload, signature, public_key_path) | |
begin | |
# Read and parse header to get algorithm | |
header_json = JSON.parse(decode_base64url(header)) | |
algorithm = header_json['alg'] | |
# Read public key and create appropriate key object based on algorithm | |
public_key_pem = File.read(public_key_path) | |
# Determine expected key type based on algorithm | |
public_key = case algorithm | |
when 'RS256', 'RS384', 'RS512' | |
# RSA algorithms require RSA keys | |
OpenSSL::PKey::RSA.new(public_key_pem) | |
when 'ES256', 'ES384', 'ES512' | |
# ECDSA algorithms require EC keys | |
OpenSSL::PKey::EC.new(public_key_pem) | |
else | |
# Unsupported algorithm | |
$stderr.puts "Unsupported algorithm: #{algorithm}" | |
return false | |
end | |
# Prepare data to verify (header.payload) | |
data = "#{header}.#{payload}" | |
# Decode signature | |
decoded_signature = decode_base64url(signature) | |
# Verify based on algorithm | |
case algorithm | |
when 'RS256' | |
digest = OpenSSL::Digest::SHA256.new | |
return public_key.verify(digest, decoded_signature, data) | |
when 'RS384' | |
digest = OpenSSL::Digest::SHA384.new | |
return public_key.verify(digest, decoded_signature, data) | |
when 'RS512' | |
digest = OpenSSL::Digest::SHA512.new | |
return public_key.verify(digest, decoded_signature, data) | |
when 'ES256' | |
digest = OpenSSL::Digest::SHA256.new | |
der_signature = convert_ecdsa_signature_to_der(decoded_signature, public_key.group.degree) | |
return false if der_signature.nil? | |
return public_key.verify(digest, der_signature, data) | |
when 'ES384' | |
digest = OpenSSL::Digest::SHA384.new | |
der_signature = convert_ecdsa_signature_to_der(decoded_signature, public_key.group.degree) | |
return false if der_signature.nil? | |
return public_key.verify(digest, der_signature, data) | |
when 'ES512' | |
digest = OpenSSL::Digest::SHA512.new | |
der_signature = convert_ecdsa_signature_to_der(decoded_signature, public_key.group.degree) | |
return false if der_signature.nil? | |
return public_key.verify(digest, der_signature, data) | |
else | |
$stderr.puts "Unsupported algorithm: #{algorithm}" | |
return false | |
end | |
rescue | |
$stderr.puts "Error verifying signature: #{$!}" | |
return false | |
end | |
end | |
# Parse command line options | |
options = {} | |
OptionParser.new do |opts| | |
opts.on("-v", "--verify PUBLIC_KEY_PATH", "Verify JWT signature using public key file") do |path| | |
options[:verify] = path | |
end | |
end.parse! | |
# Read JWT from stdin and split once | |
input = ARGF.read.strip | |
header, payload, signature = input.split(".", 3) | |
# Original output - decode header and payload | |
puts Base64.decode64(header) | |
puts Base64.decode64(payload) | |
# Add signature verification if -v flag is provided | |
if options[:verify] | |
if File.exist?(options[:verify]) | |
if verify_signature(header, payload, signature, options[:verify]) | |
puts "\e[32m✓\e[0m Valid signature" | |
else | |
puts "\e[31m✗\e[0m Invalid signature" | |
end | |
else | |
$stderr.puts "Unable to validate signature: public key file not found at #{options[:verify]}" | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment