diff --git a/Gemfile.lock b/Gemfile.lock index 1e9af9e8..75dd4774 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - health_cards (1.1.0) + health_cards (1.1.1) fhir_models (>= 4.0.0) rqrcode rqrcode_core (>= 1.2.0) @@ -9,220 +9,244 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (6.1.3.1) - actionpack (= 6.1.3.1) - activesupport (= 6.1.3.1) + actioncable (6.1.3.2) + actionpack (= 6.1.3.2) + activesupport (= 6.1.3.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.3.1) - actionpack (= 6.1.3.1) - activejob (= 6.1.3.1) - activerecord (= 6.1.3.1) - activestorage (= 6.1.3.1) - activesupport (= 6.1.3.1) + actionmailbox (6.1.3.2) + actionpack (= 6.1.3.2) + activejob (= 6.1.3.2) + activerecord (= 6.1.3.2) + activestorage (= 6.1.3.2) + activesupport (= 6.1.3.2) mail (>= 2.7.1) - actionmailer (6.1.3.1) - actionpack (= 6.1.3.1) - actionview (= 6.1.3.1) - activejob (= 6.1.3.1) - activesupport (= 6.1.3.1) + actionmailer (6.1.3.2) + actionpack (= 6.1.3.2) + actionview (= 6.1.3.2) + activejob (= 6.1.3.2) + activesupport (= 6.1.3.2) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.3.1) - actionview (= 6.1.3.1) - activesupport (= 6.1.3.1) + actionpack (6.1.3.2) + actionview (= 6.1.3.2) + activesupport (= 6.1.3.2) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.3.1) - actionpack (= 6.1.3.1) - activerecord (= 6.1.3.1) - activestorage (= 6.1.3.1) - activesupport (= 6.1.3.1) + actiontext (6.1.3.2) + actionpack (= 6.1.3.2) + activerecord (= 6.1.3.2) + activestorage (= 6.1.3.2) + activesupport (= 6.1.3.2) nokogiri (>= 1.8.5) - actionview (6.1.3.1) - activesupport (= 6.1.3.1) + actionview (6.1.3.2) + activesupport (= 6.1.3.2) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.3.1) - activesupport (= 6.1.3.1) + activejob (6.1.3.2) + activesupport (= 6.1.3.2) globalid (>= 0.3.6) - activemodel (6.1.3.1) - activesupport (= 6.1.3.1) - activerecord (6.1.3.1) - activemodel (= 6.1.3.1) - activesupport (= 6.1.3.1) - activestorage (6.1.3.1) - actionpack (= 6.1.3.1) - activejob (= 6.1.3.1) - activerecord (= 6.1.3.1) - activesupport (= 6.1.3.1) + activemodel (6.1.3.2) + activesupport (= 6.1.3.2) + activerecord (6.1.3.2) + activemodel (= 6.1.3.2) + activesupport (= 6.1.3.2) + activestorage (6.1.3.2) + actionpack (= 6.1.3.2) + activejob (= 6.1.3.2) + activerecord (= 6.1.3.2) + activesupport (= 6.1.3.2) marcel (~> 1.0.0) mini_mime (~> 1.0.2) - activesupport (6.1.3.1) + activesupport (6.1.3.2) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.7.0) - public_suffix (>= 2.0.2, < 5.0) - american_date (1.1.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + american_date (1.3.0) ast (2.4.2) bcp47 (0.3.3) i18n + bigdecimal (3.1.8) bindex (0.8.1) - bootsnap (1.7.2) - msgpack (~> 1.0) - builder (3.2.4) - bulma-rails (0.9.1) + bootsnap (1.18.4) + msgpack (~> 1.2) + builder (3.3.0) + bulma-rails (0.9.4) sassc (~> 2.0) byebug (11.1.3) - capybara (3.35.3) + capybara (3.39.2) addressable + matrix mini_mime (>= 0.1.3) nokogiri (~> 1.8) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - childprocess (3.0.0) chunky_png (1.4.0) - codecov (0.5.2) + codecov (0.6.0) simplecov (>= 0.15, < 0.22) coderay (1.1.3) - concurrent-ruby (1.1.9) - crack (0.4.5) + concurrent-ruby (1.3.4) + crack (1.0.0) + bigdecimal rexml crass (1.0.6) + date (3.3.4) date_time_precision (0.8.1) - docile (1.3.5) - dotenv (2.7.6) - dotenv-rails (2.7.6) - dotenv (= 2.7.6) + docile (1.4.1) + dotenv (2.8.1) + dotenv-rails (2.8.1) + dotenv (= 2.8.1) railties (>= 3.2) - erubi (1.10.0) - faker (2.17.0) - i18n (>= 1.6, < 2) - ffi (1.15.0) - fhir_models (4.2.0) + erubi (1.13.0) + faker (3.4.2) + i18n (>= 1.8.11, < 2) + ffi (1.17.0) + fhir_models (4.2.2) bcp47 (>= 0.3) date_time_precision (>= 0.8) mime-types (>= 3.0) nokogiri (>= 1.11.4) - font-awesome-rails (4.7.0.7) - railties (>= 3.2, < 7) - globalid (0.4.2) - activesupport (>= 4.2.0) - hashdiff (1.0.1) - i18n (1.8.10) + font-awesome-rails (4.7.0.8) + railties (>= 3.2, < 8.0) + globalid (1.2.1) + activesupport (>= 6.1) + hashdiff (1.1.1) + i18n (1.14.6) concurrent-ruby (~> 1.0) - jbuilder (2.11.2) + jbuilder (2.13.0) + actionview (>= 5.0.0) activesupport (>= 5.0.0) - listen (3.4.1) + json (2.7.2) + language_server-protocol (3.17.0.3) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.9.0) + loofah (2.22.0) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + mail (2.8.1) mini_mime (>= 0.1.1) - marcel (1.0.0) - method_source (1.0.0) - mime-types (3.3.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + matrix (0.4.2) + method_source (1.1.0) + mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2021.0225) + mime-types-data (3.2024.0903) mini_mime (1.0.3) - mini_portile2 (2.5.3) - minitest (5.14.4) - msgpack (1.4.2) - nio4r (2.5.7) - nokogiri (1.11.7) - mini_portile2 (~> 2.5.0) + mini_portile2 (2.8.7) + minitest (5.25.1) + msgpack (1.7.2) + net-imap (0.4.16) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.0) + net-protocol + nio4r (2.7.3) + nokogiri (1.15.6) + mini_portile2 (~> 2.8.2) racc (~> 1.4) - parallel (1.20.1) - parser (3.0.0.0) + parallel (1.26.3) + parser (3.3.5.0) ast (~> 2.4.1) - pry (0.14.0) + racc + pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - pry-byebug (3.8.0) + pry-byebug (3.10.1) byebug (~> 11.0) - pry (~> 0.10) - public_suffix (4.0.6) - puma (5.2.2) + pry (>= 0.13, < 0.15) + public_suffix (5.1.1) + puma (5.6.9) nio4r (~> 2.0) - racc (1.5.2) - rack (2.2.3) - rack-cors (1.1.1) + racc (1.8.1) + rack (2.2.9) + rack-cors (2.0.2) rack (>= 2.0.0) - rack-mini-profiler (2.3.1) + rack-mini-profiler (2.3.4) rack (>= 1.2.0) - rack-proxy (0.6.5) + rack-proxy (0.7.7) rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.1.3.1) - actioncable (= 6.1.3.1) - actionmailbox (= 6.1.3.1) - actionmailer (= 6.1.3.1) - actionpack (= 6.1.3.1) - actiontext (= 6.1.3.1) - actionview (= 6.1.3.1) - activejob (= 6.1.3.1) - activemodel (= 6.1.3.1) - activerecord (= 6.1.3.1) - activestorage (= 6.1.3.1) - activesupport (= 6.1.3.1) + rack-test (2.1.0) + rack (>= 1.3) + rails (6.1.3.2) + actioncable (= 6.1.3.2) + actionmailbox (= 6.1.3.2) + actionmailer (= 6.1.3.2) + actionpack (= 6.1.3.2) + actiontext (= 6.1.3.2) + actionview (= 6.1.3.2) + activejob (= 6.1.3.2) + activemodel (= 6.1.3.2) + activerecord (= 6.1.3.2) + activestorage (= 6.1.3.2) + activesupport (= 6.1.3.2) bundler (>= 1.15.0) - railties (= 6.1.3.1) + railties (= 6.1.3.2) sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (6.1.3.1) - actionpack (= 6.1.3.1) - activesupport (= 6.1.3.1) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (6.1.3.2) + actionpack (= 6.1.3.2) + activesupport (= 6.1.3.2) method_source rake (>= 0.8.7) thor (~> 1.0) - rainbow (3.0.0) - rake (13.0.3) - rb-fsevent (0.10.4) - rb-inotify (0.10.1) + rainbow (3.1.1) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) ffi (~> 1.0) - regexp_parser (2.1.1) - rexml (3.2.4) - rqrcode (2.1.0) + regexp_parser (2.9.2) + rexml (3.3.7) + rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rubocop (1.11.0) + rubocop (1.66.1) + json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.0.0.0) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.2.0, < 2.0) + regexp_parser (>= 2.4, < 3.0) + rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.4.1) - parser (>= 2.7.1.5) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.32.3) + parser (>= 3.3.1.0) rubocop-minitest (0.10.3) rubocop (>= 0.87, < 2.0) - rubocop-rails (2.9.1) + rubocop-rails (2.26.2) activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 0.90.0, < 2.0) + rubocop (>= 1.52.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) rubocop-rake (0.5.1) rubocop - ruby-progressbar (1.11.0) - rubyzip (2.3.0) + ruby-progressbar (1.13.0) + rubyzip (2.3.2) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) @@ -233,60 +257,64 @@ GEM sprockets (> 3.0) sprockets-rails tilt - selenium-webdriver (3.142.7) - childprocess (>= 0.5, < 4.0) - rubyzip (>= 1.2.2) + selenium-webdriver (4.9.0) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) semantic_range (3.0.0) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) - simplecov_json_formatter (0.1.2) - spring (2.1.1) - sprockets (4.0.2) + simplecov-html (0.13.1) + simplecov_json_formatter (0.1.4) + spring (4.2.1) + sprockets (4.2.1) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) - sqlite3 (1.4.2) - thor (1.1.0) - tilt (2.0.10) + sqlite3 (1.7.3) + mini_portile2 (~> 2.8.0) + thor (1.3.2) + tilt (2.4.0) + timeout (0.4.1) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) - tzinfo (2.0.4) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.0.0) - web-console (4.1.0) + unicode-display_width (2.6.0) + web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webdrivers (4.6.0) + webdrivers (5.3.1) nokogiri (~> 1.6) rubyzip (>= 1.3.0) - selenium-webdriver (>= 3.0, < 4.0) - webmock (3.12.2) - addressable (>= 2.3.6) + selenium-webdriver (~> 4.0, < 4.11) + webmock (3.23.1) + addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webpacker (5.2.1) + webpacker (5.4.4) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) - websocket-driver (0.7.3) + websocket (1.2.11) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - wicked_pdf (2.1.0) + wicked_pdf (2.8.1) activesupport - wkhtmltopdf-binary (0.12.6.5) + wkhtmltopdf-binary (0.12.6.8) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.4.2) + zeitwerk (2.6.18) PLATFORMS ruby diff --git a/lib/health_cards/key.rb b/lib/health_cards/key.rb index 0425404c..4c0d9b7d 100644 --- a/lib/health_cards/key.rb +++ b/lib/health_cards/key.rb @@ -21,6 +21,63 @@ def self.enforce_valid_key_type!(obj, allow_nil: false) # @param jwk_key [Hash] The JWK represented by a Hash # @return [HealthCards::Key] The key represented by the JWK def self.from_jwk(jwk_key) + return Key.from_jwk_openssl3(jwk_key) if Key.openssl_3? + + Key.from_jwk_openssl1(jwk_key) + end + + def self.from_jwk_openssl3(jwk_key) + # Largely taken, then slightly modified from + # https://github.com/jwt/ruby-jwt/blob/main/lib/jwt/jwk/ec.rb#L131 on 2022-01-17 + jwk_key = jwk_key.transform_keys(&:to_sym) + curve = 'prime256v1' + + x_octets = Base64.urlsafe_decode64(jwk_key[:x]) + y_octets = Base64.urlsafe_decode64(jwk_key[:y]) + + point = OpenSSL::PKey::EC::Point.new( + OpenSSL::PKey::EC::Group.new(curve), + OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2) + ) + + sequence = if jwk_key.key?(:d) + d_octets = Base64.urlsafe_decode64(jwk_key[:d]) + Key.jwk_ec_asn1_seq(curve, point, d_octets) + else + Key.jwk_ec_asn1_seq(curve, point) + end + + key = OpenSSL::PKey::EC.new(sequence.to_der) + key.private_key? ? HealthCards::PrivateKey.new(key) : HealthCards::PublicKey.new(key) + end + + def self.jwk_ec_asn1_seq(curve, point, d_octets = nil) + if d_octets.nil? + # Public key + OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), + OpenSSL::ASN1::ObjectId(curve)]), + OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed)) + ]) + else + # https://datatracker.ietf.org/doc/html/rfc5915.html + # ECPrivateKey ::= SEQUENCE { + # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), + # privateKey OCTET STRING, + # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, + # publicKey [1] BIT STRING OPTIONAL + # } + OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Integer(1), + OpenSSL::ASN1::OctetString(OpenSSL::BN.new(d_octets, 2).to_s(2)), + OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT), + OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, + :EXPLICIT) + ]) + end + end + + def self.from_jwk_openssl1(jwk_key) jwk_key = jwk_key.transform_keys(&:to_sym) group = OpenSSL::PKey::EC::Group.new('prime256v1') key = OpenSSL::PKey::EC.new(group) @@ -30,6 +87,10 @@ def self.from_jwk(jwk_key) key.private_key? ? HealthCards::PrivateKey.new(key) : HealthCards::PublicKey.new(key) end + def self.openssl_3? + OpenSSL::OPENSSL_VERSION_NUMBER >= 3 * 0x10000000 + end + def initialize(ec_key) @key = ec_key end diff --git a/lib/health_cards/private_key.rb b/lib/health_cards/private_key.rb index 60120769..231e845d 100644 --- a/lib/health_cards/private_key.rb +++ b/lib/health_cards/private_key.rb @@ -29,13 +29,35 @@ def sign(payload) def public_key return @public_key if @public_key - pub = OpenSSL::PKey::EC.new('prime256v1') - pub.public_key = @key.public_key - @public_key = PublicKey.new(pub) + @public_key = if Key.openssl_3? + public_key_openssl3 + else + public_key_openssl1 + end end private + def public_key_openssl3 + # Largely taken, then slightly modified from + # https://github.com/jwt/ruby-jwt/blob/main/lib/jwt/jwk/ec.rb#L131 on 2022-01-17 + curve = 'prime256v1' + point = @key.public_key + sequence = OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), + OpenSSL::ASN1::ObjectId(curve)]), + OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed)) + ]) + pub = OpenSSL::PKey::EC.new(sequence.to_der) + PublicKey.new(pub) + end + + def public_key_openssl1 + pub = OpenSSL::PKey::EC.new('prime256v1') + pub.public_key = @key.public_key + PublicKey.new(pub) + end + # Convert the ASN.1 Representation into the raw signature # # Adapted from ruby-jwt and json-jwt gems. More info here: diff --git a/lib/health_cards/version.rb b/lib/health_cards/version.rb index 677b2f51..418f9a92 100644 --- a/lib/health_cards/version.rb +++ b/lib/health_cards/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module HealthCards - VERSION = '1.1.0' + VERSION = '1.1.2' end