Skip to content

Commit

Permalink
OPK and CI
Browse files Browse the repository at this point in the history
  • Loading branch information
anakinj committed Aug 2, 2024
1 parent 287972e commit eb7077a
Show file tree
Hide file tree
Showing 12 changed files with 358 additions and 58 deletions.
74 changes: 50 additions & 24 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,53 @@
name: Ruby
---
name: test
on:
push:
branches:
- "*"
pull_request:
branches:
- "*"
jobs:
lint:
name: RuboCop
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true
- name: Run RuboCop
run: bundle exec rubocop
test:
name: Ruby ${{ matrix.ruby }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ruby:
- "2.5"
- "2.6"
- "2.7"
- "3.0"
- "3.1"
- "3.2"
- "3.3"
steps:
- uses: actions/checkout@v3

on:
push:
branches:
- master
- name: Install libsodium
run: |
sudo apt-get update -q
sudo apt-get install libsodium-dev -y
pull_request:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true

jobs:
build:
runs-on: ubuntu-latest
name: Ruby ${{ matrix.ruby }}
strategy:
matrix:
ruby:
- '3.3.0'

steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run the default task
run: bundle exec rake
- name: Run tests
run: bundle exec rspec
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ gem "jwt", github: "anakinj/ruby-jwt", branch: "extendable-algos"
gem "rake"
gem "rspec"
gem "rubocop"
gem "simplecov"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# jwt-eddsa

A library extending the ruby-jwt gem with EdDSA algorithms
A library extending the ruby-jwt gem with EdDSA algorithms. Based on [RFC 8037](https://datatracker.ietf.org/doc/html/rfc8037).

**NOTE: This gem is still WIP**

Expand Down
5 changes: 3 additions & 2 deletions jwt-eddsa.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ require_relative "lib/jwt/eddsa/version"

Gem::Specification.new do |spec|
spec.name = "jwt-eddsa"
spec.version = JWT::Eddsa::VERSION
spec.version = JWT::EdDSA::VERSION
spec.authors = ["Joakim Antman"]
spec.email = ["[email protected]"]

Expand All @@ -15,7 +15,7 @@ Gem::Specification.new do |spec|

spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/anakinj/jwt-eddsa"
spec.metadata["changelog_uri"] = "https://github.com/anakinj/jwt-eddsablob/v#{JWT::Eddsa::VERSION}/CHANGELOG.md"
spec.metadata["changelog_uri"] = "https://github.com/anakinj/jwt-eddsablob/v#{JWT::EdDSA::VERSION}/CHANGELOG.md"

# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
Expand All @@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_dependency "base64"
spec.add_dependency "jwt", "> 2.8.2"
spec.add_dependency "rbnacl", "~> 6.0"

Expand Down
32 changes: 3 additions & 29 deletions lib/jwt/eddsa.rb
Original file line number Diff line number Diff line change
@@ -1,33 +1,7 @@
# frozen_string_literal: true

require "jwt"
require_relative "eddsa/version"

module JWT
# EdDSA algorithm implementation
module Eddsa
include JWT::JWA::Algorithm

register_algorithm("EdDSA")

class << self
def sign(_algorithm, msg, key)
unless key.is_a?(RbNaCl::Signatures::Ed25519::SigningKey)
raise_sign_error!("Key given is a #{key.class} but needs to be a RbNaCl::Signatures::Ed25519::SigningKey")
end

key.sign(msg)
end

def verify(_algorithm, public_key, signing_input, signature)
unless public_key.is_a?(RbNaCl::Signatures::Ed25519::VerifyKey)
raise_verify_error!("Key given is a #{public_key.class} but needs to be a RbNaCl::Signatures::Ed25519::VerifyKey")
end

public_key.verify(signature, signing_input)
rescue RbNaCl::CryptoError
false
end
end
end
end
require_relative "eddsa/version"
require_relative "eddsa/jwk/okp"
require_relative "eddsa/algo"
34 changes: 34 additions & 0 deletions lib/jwt/eddsa/algo.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module JWT
module EdDSA
# EdDSA algorithm implementation
module Algo
include JWT::JWA::Algorithm

register_algorithm("EdDSA")

class << self
def sign(_algorithm, msg, key)
unless key.is_a?(RbNaCl::Signatures::Ed25519::SigningKey)
raise_sign_error!("Key given is a #{key.class} but needs to be a " \
"RbNaCl::Signatures::Ed25519::SigningKey")
end

key.sign(msg)
end

def verify(_algorithm, public_key, signing_input, signature)
unless public_key.is_a?(RbNaCl::Signatures::Ed25519::VerifyKey)
raise_verify_error!("Key given is a #{public_key.class} but needs to be a " \
"RbNaCl::Signatures::Ed25519::VerifyKey")
end

public_key.verify(signature, signing_input)
rescue RbNaCl::CryptoError
false
end
end
end
end
end
121 changes: 121 additions & 0 deletions lib/jwt/eddsa/jwk/okp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# frozen_string_literal: true

module JWT
module EdDSA
module JWK
# https://datatracker.ietf.org/doc/html/rfc8037
class OKP < ::JWT::JWK::KeyBase
KTY = "OKP"
KTYS = [KTY, JWT::EdDSA::JWK::OKP, RbNaCl::Signatures::Ed25519::SigningKey,
RbNaCl::Signatures::Ed25519::VerifyKey].freeze
OKP_PUBLIC_KEY_ELEMENTS = %i[kty n x].freeze
OKP_PRIVATE_KEY_ELEMENTS = %i[d].freeze

def initialize(key, params = nil, options = {})
params ||= {}
# For backwards compatibility when kid was a String
params = { kid: params } if params.is_a?(String)

key_params = extract_key_params(key)

params = params.transform_keys(&:to_sym)
check_jwk_params!(key_params, params)
super(options, key_params.merge(params))
end

def verify_key
return @verify_key if defined?(@verify_key)

@verify_key = verify_key_from_parameters
end

def signing_key
return @signing_key if defined?(@signing_key)

@signing_key = signing_key_from_parameters
end

def key_digest
::JWT::JWK::Thumbprint.new(self).to_s
end

def private?
!signing_key.nil?
end

def members
OKP_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
end

def export(options = {})
exported = parameters.clone
unless private? && options[:include_private] == true
exported.reject! do |k, _|
OKP_PRIVATE_KEY_ELEMENTS.include?(k)
end
end
exported
end

private

def extract_key_params(key) # rubocop:disable Metric/MethodLength
case key
when JWT::JWK::KeyBase
key.export(include_private: true)
when RbNaCl::Signatures::Ed25519::SigningKey
@signing_key = key
@verify_key = key.verify_key
parse_okp_key_params(@verify_key, @signing_key)
when RbNaCl::Signatures::Ed25519::VerifyKey
@signing_key = nil
@verify_key = key
parse_okp_key_params(@verify_key)
when Hash
key.transform_keys(&:to_sym)
else
raise ArgumentError,
"key must be of type RbNaCl::Signatures::Ed25519::SigningKey, " \
"RbNaCl::Signatures::Ed25519::VerifyKey " \
"or Hash with key parameters"
end
end

def check_jwk_params!(key_params, _given_params)
return if key_params[:kty] == KTY

raise JWT::JWKError,
"Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}"
end

def parse_okp_key_params(verify_key, signing_key = nil)
params = {
kty: KTY,
crv: "Ed25519",
x: ::Base64.urlsafe_encode64(verify_key.to_bytes, padding: false)
}

params[:d] = ::Base64.urlsafe_encode64(signing_key.to_bytes, padding: false) if signing_key

params
end

def verify_key_from_parameters
RbNaCl::Signatures::Ed25519::VerifyKey.new(::Base64.urlsafe_decode64(self[:x]))
end

def signing_key_from_parameters
return nil unless self[:d]

RbNaCl::Signatures::Ed25519::SigningKey.new(::Base64.urlsafe_decode64(self[:d]))
end

class << self
def import(jwk_data)
new(jwk_data)
end
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/jwt/eddsa/version.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module JWT
module Eddsa
module EdDSA
VERSION = "0.2.0"
end
end
17 changes: 17 additions & 0 deletions spec/integration_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "securerandom"

RSpec.describe "Usage via ruby-jwt" do
let(:private_key) { RbNaCl::Signatures::Ed25519::SigningKey.new("b" * 32) }
let(:public_key) { private_key.verify_key }
Expand Down Expand Up @@ -44,4 +46,19 @@
end
end
end

describe "OKP JWK usage" do
let(:jwk) { JWT::JWK.new(RbNaCl::Signatures::Ed25519::SigningKey.new(SecureRandom.hex)) }
let(:public_jwks) { { keys: [jwk.export, { kid: "not_the_correct_one", kty: "oct", k: "secret" }] } }
let(:signed_token) { JWT.encode(token_payload, jwk.signing_key, "EdDSA", token_headers) }
let(:token_payload) { { "data" => "something" } }
let(:token_headers) { { kid: jwk.kid } }

it "decodes the token" do
key_loader = ->(_options) { JSON.parse(JSON.generate(public_jwks)) }
payload, _header = JWT.decode(signed_token, nil, true,
{ algorithms: ["EDDSA"], jwks: key_loader })
expect(payload).to eq(token_payload)
end
end
end
2 changes: 1 addition & 1 deletion spec/jwt/eddsa_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

RSpec.describe JWT::Eddsa do
RSpec.describe JWT::EdDSA do
it "has a version number" do
expect(described_class::VERSION).not_to be nil
end
Expand Down
Loading

0 comments on commit eb7077a

Please sign in to comment.