From a443c44ef3f5b09dd267f08991e5bd2248a07373 Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Fri, 14 Jun 2019 15:53:03 +1000 Subject: [PATCH 1/7] add RSA signature support --- src/jwt.cr | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/jwt.cr b/src/jwt.cr index 6aaaffc..370a29e 100644 --- a/src/jwt.cr +++ b/src/jwt.cr @@ -1,6 +1,7 @@ require "json" require "base64" require "openssl/hmac" +require "openssl_ext/rsa" require "./jwt/*" @@ -73,6 +74,18 @@ module JWT OpenSSL::HMAC.digest(:sha384, key, data) when "HS512" OpenSSL::HMAC.digest(:sha512, key, data) + when "RS256" + rsa = OpenSSL::RSA.new(key) + digest = OpenSSL::Digest.new(:sha256) + rsa.sign(digest, data) + when "RS384" + rsa = OpenSSL::RSA.new(key) + digest = OpenSSL::Digest.new(:sha384) + rsa.sign(digest, data) + when "RS512" + rsa = OpenSSL::RSA.new(key) + digest = OpenSSL::Digest.new(:sha512) + rsa.sign(digest, data) else raise(UnsupportedAlogrithmError.new("Unsupported algorithm: #{algorithm}")) end end From 8f9b565b07b094840362f5f8a64629b94f1c6a5a Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Fri, 14 Jun 2019 15:54:29 +1000 Subject: [PATCH 2/7] add openssl_ext dependency --- shard.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shard.yml b/shard.yml index a525260..7efeb8b 100644 --- a/shard.yml +++ b/shard.yml @@ -4,4 +4,8 @@ version: 0.4.0 authors: - Potapov Sergey +dependencies: + openssl_ext: + github: randomstate/openssl_ext + license: MIT From bcd16ad1ab5cfd5ec36d132a7fd7e10be23a780c Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Fri, 14 Jun 2019 17:28:23 +1000 Subject: [PATCH 3/7] fix RSA digest --- .gitignore | 2 +- src/jwt.cr | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index c8cf75a..170867a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /doc/ +/lib/ /libs/ /.crystal/ /.shards/ @@ -7,4 +8,3 @@ # Libraries don't need dependency lock # Dependencies will be locked in application that uses them /shard.lock - diff --git a/src/jwt.cr b/src/jwt.cr index 370a29e..3f666ad 100644 --- a/src/jwt.cr +++ b/src/jwt.cr @@ -1,7 +1,7 @@ require "json" require "base64" require "openssl/hmac" -require "openssl_ext/rsa" +require "openssl_ext" require "./jwt/*" @@ -76,15 +76,15 @@ module JWT OpenSSL::HMAC.digest(:sha512, key, data) when "RS256" rsa = OpenSSL::RSA.new(key) - digest = OpenSSL::Digest.new(:sha256) + digest = OpenSSL::Digest.new("sha256") rsa.sign(digest, data) when "RS384" rsa = OpenSSL::RSA.new(key) - digest = OpenSSL::Digest.new(:sha384) + digest = OpenSSL::Digest.new("sha384") rsa.sign(digest, data) when "RS512" rsa = OpenSSL::RSA.new(key) - digest = OpenSSL::Digest.new(:sha512) + digest = OpenSSL::Digest.new("sha512") rsa.sign(digest, data) else raise(UnsupportedAlogrithmError.new("Unsupported algorithm: #{algorithm}")) end From fb6fb926d227c2c053ebc1b97a1e1ad98c2bcfb3 Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Sat, 15 Jun 2019 11:42:25 +1000 Subject: [PATCH 4/7] add RSA spec --- spec/integration/algorithms/rsa_spec.cr | 64 +++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 spec/integration/algorithms/rsa_spec.cr diff --git a/spec/integration/algorithms/rsa_spec.cr b/spec/integration/algorithms/rsa_spec.cr new file mode 100644 index 0000000..60c321e --- /dev/null +++ b/spec/integration/algorithms/rsa_spec.cr @@ -0,0 +1,64 @@ +require "../../spec_helper" + +describe JWT do + secret_key = "-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQC5+5+xnWggxNnnmCSNbIwTQFjcyawcvmPupeXs10sfhUAHUxtm +T5zH3AI46JrRZN7KV5Ac5bQWzF9ZMPeHqmq5FBdYooIF8W7lVtYx23OQX5vjFRN0 +LRY8hyOKL07Us+aUeMwDXX7M6o58XO4bqOh8pGOqFLscCAkdAP9lDgeDGwIDAQAB +AoGAcRt/jnSNbEhrwXZ83GmkctzSbkxUWRLNEclhIP36WQwf2ZSIeFt4nO/Hhjao +WSqAeAxyv7BPKwJWBpdKIv7Ycfbu2c1JxWgacuotewMk5IYPXUs89QY3AL5I4BJd +Zqd3o9K4OWwakukkfjxHKFC/grifNa4yVQ6IZn+XuW/AspkCQQDlmzkUapzg0n0t +3gmK6KQD9f5YdXKYGYzYO3Scrtrz53fewqfDXdLC7TGL9qw9vGEFvSE727vwR3X+ ++DZ6RWYvAkEAz1yqUNnrPwzGx3JuINIXgfzGTq4gSf+xRjb5qDJUPnMt4I3PrPyV +pm34aUCgo26go2+itBGjzFDaJCOT4izi1QJAJq6E6kSf01yCzFRo5ScWYrhxtjNr +L+a2DMPPfIoUxxyK3FOM8eP/mulc/Ih9MhVnfxEC5VO6kNtpLKBihSzl7wJBAJrR +4eu5uJV7kZJqEmV41spbkyg9g6gcOxxkgWQeJ5302wT0fGD4uTbolnbnJMjBGTjN +adot7XDn0Ob4lTpiLv0CQQDkECppYQ4N0ecegg1xPVqf19fHo/WGHGuScjfUPTI/ +k0LaJjYM2ycehinmuLHgY3qdDJgtEbt4WG5XNQzhyfaN +-----END RSA PRIVATE KEY-----" + + wrong_key = OpenSSL::RSA.new(1024).to_pem + payload = {"foo" => "bar"} + + algorithms = [ + ["RS256", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.rJUpucYWdjmGiVGHrU4TMwfYNcF52Hm1Q4hJfHhfUPvVL-S0fRHRgwNns90MDOFReXH8_6swbtezzeuQleSY-NdYLEvnXwYHzjLP-Bxc3mrKNMnf8ta1lYB7NqdnIu2nqcNjflJBubn5sIi7-zZew_ohqgMP8H7ptDuICr7ibGQ"], + ["RS384", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9.eyJmb28iOiJiYXIifQ.FfdS8chkIE-PRU61h8VLZgVYvKI3yAvaEpGjqDP0ypGa_0rF6iOCkRuEByhBsH-lCVmKcU-1bp3OsEGXtuYlthpklM76gDDP4YMss2mdH4_xr6P9UQ7lL_xb8inOCbnNMsm7xecIPElDkJ5W22iwF2fbi67p9hlJwgcfBsyfqX4"], + ["RS512", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJmb28iOiJiYXIifQ.StG6Du1SpGrP7BdFyW6VjMwHudEdekdlJjbT1ByWFPerp7hZ1P7ukHOFMzVVOm6e0xLO6XGk11jDvC_zG2wunjEoMKYY_DuSmUOjVcZVz5m5korH9PJNJRREoQPa42QTVUaMeuv8A3xlq6_SG9wLCGVib4JsIFyS1qPzS3PlNZg"], + ] + + algorithms.each do |alg_data| + alg, expected_token = alg_data + + describe "algorithm #{alg}" do + it "generates proper token, that can be decoded" do + token = JWT.encode(payload, secret_key, alg) + token.should eq(expected_token) + + decoded_token = JWT.decode(token, secret_key, alg) + decoded_token[0].should eq(payload) + decoded_token[1].should eq({"typ" => "JWT", "alg" => alg}) + end + + describe "#decode" do + context "when token was signed with another key" do + it "raises JWT::VerificationError" do + token = JWT.encode(payload, wrong_key, alg) + expect_raises(JWT::VerificationError, "Signature verification failed") do + JWT.decode(token, secret_key, alg) + end + end + end + + context "when token contains not 3 segments" do + it "raises JWT::DecodeError" do + ["e30", "e30.e30", "e30.e30.e30.e30"].each do |invalid_token| + expect_raises(JWT::DecodeError) do + JWT.decode(invalid_token, secret_key, alg) + end + end + end + end + end + end + end +end From c36cdb4e04b7c47f2d877bb608d5300230e6cccf Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Sat, 15 Jun 2019 11:43:40 +1000 Subject: [PATCH 5/7] update supported algorithms --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bcc7013..ab63c87 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ payload, header = JWT.decode(token, "$secretKey", "HS256") ## Supported algorithms * [x] none * [x] HMAC (HS256, HS384, HS512) -* [ ] RSA - in progress +* [x] RSA (RS256, RS384, RS512) ## Supported reserved claim names JSON Web Token defines some reserved claim names and how they should be used. From d5af994ef05e757162a92c4cf7ad9e2fca4ed940 Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Sat, 15 Jun 2019 11:48:22 +1000 Subject: [PATCH 6/7] use patched openssl_ext --- shard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shard.yml b/shard.yml index 7efeb8b..33158eb 100644 --- a/shard.yml +++ b/shard.yml @@ -6,6 +6,6 @@ authors: dependencies: openssl_ext: - github: randomstate/openssl_ext + github: stakach/openssl_ext license: MIT From 90a7fbaa02fa49f41467048a4738db65bd56f05e Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Sun, 16 Jun 2019 00:06:24 +1000 Subject: [PATCH 7/7] add support for custom header keys --- src/jwt.cr | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/jwt.cr b/src/jwt.cr index 3f666ad..4910c39 100644 --- a/src/jwt.cr +++ b/src/jwt.cr @@ -8,9 +8,9 @@ require "./jwt/*" module JWT extend self - def encode(payload, key : String, algorithm : String) : String + def encode(payload, key : String, algorithm : String, **header_keys) : String segments = [] of String - segments << encode_header(algorithm) + segments << encode_header(algorithm, **header_keys) segments << encode_payload(payload) segments << encoded_signature(algorithm, key, segments.join(".")) segments.join(".") @@ -49,10 +49,9 @@ module JWT raise DecodeError.new("Invalid JSON") end - def encode_header(algorithm : String) : String - header = {"typ" => "JWT", "alg" => algorithm} - json = header.to_json - base64_encode(json) + def encode_header(algorithm : String, **keys) : String + header = {typ: "JWT", alg: algorithm}.merge(keys) + base64_encode(header.to_json) end def encode_payload(payload) : String