class JWT::JWK::EC

Constants

BINARY
EC_KEY_ELEMENTS
EC_PRIVATE_KEY_ELEMENTS
EC_PUBLIC_KEY_ELEMENTS
KTY
KTYS
ZERO_BYTE

Public Class Methods

new(key, params = nil, options = {}) click to toggle source
Calls superclass method
# File lib/jwt/jwk/ec.rb, line 16
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

Private Class Methods

import(jwk_data) click to toggle source
# File lib/jwt/jwk/ec.rb, line 232
def import(jwk_data)
  new(jwk_data)
end
to_openssl_curve(crv) click to toggle source
# File lib/jwt/jwk/ec.rb, line 236
def to_openssl_curve(crv)
  # The JWK specs and OpenSSL use different names for the same curves.
  # See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
  # pointers on different names for common curves.
  case crv
  when 'P-256' then 'prime256v1'
  when 'P-384' then 'secp384r1'
  when 'P-521' then 'secp521r1'
  when 'P-256K' then 'secp256k1'
  else raise JWT::JWKError, 'Invalid curve provided'
  end
end

Public Instance Methods

[]=(key, value) click to toggle source
Calls superclass method
# File lib/jwt/jwk/ec.rb, line 67
def []=(key, value)
  if EC_KEY_ELEMENTS.include?(key.to_sym)
    raise ArgumentError, 'cannot overwrite cryptographic key attributes'
  end

  super(key, value)
end
export(options = {}) click to toggle source
# File lib/jwt/jwk/ec.rb, line 54
def export(options = {})
  exported = parameters.clone
  exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
  exported
end
key_digest() click to toggle source
# File lib/jwt/jwk/ec.rb, line 60
def key_digest
  _crv, x_octets, y_octets = keypair_components(ec_key)
  sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
                                      OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
end
keypair() click to toggle source
# File lib/jwt/jwk/ec.rb, line 30
def keypair
  ec_key
end
members() click to toggle source
# File lib/jwt/jwk/ec.rb, line 50
def members
  EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
end
private?() click to toggle source
# File lib/jwt/jwk/ec.rb, line 34
def private?
  ec_key.private_key?
end
public_key() click to toggle source
# File lib/jwt/jwk/ec.rb, line 46
def public_key
  ec_key
end
signing_key() click to toggle source
# File lib/jwt/jwk/ec.rb, line 38
def signing_key
  ec_key
end
verify_key() click to toggle source
# File lib/jwt/jwk/ec.rb, line 42
def verify_key
  ec_key
end

Private Instance Methods

check_jwk_params!(key_params, params) click to toggle source
# File lib/jwt/jwk/ec.rb, line 95
def check_jwk_params!(key_params, params)
  raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty?
  raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
  raise JWT::JWKError, 'Key format is invalid for EC' unless key_params[:crv] && key_params[:x] && key_params[:y]
end
create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) click to toggle source
# File lib/jwt/jwk/ec.rb, line 145
def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
  curve = EC.to_openssl_curve(jwk_crv)
  x_octets = decode_octets(jwk_x)
  y_octets = decode_octets(jwk_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_d
               # 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(decode_octets(jwk_d), 2).to_s(2)),
                                         OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT),
                                         OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
                                       ])
             else
               OpenSSL::ASN1::Sequence([
                                         OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]),
                                         OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
                                       ])
             end

  OpenSSL::PKey::EC.new(sequence.to_der)
end
decode_octets(base64_encoded_coordinate) click to toggle source
# File lib/jwt/jwk/ec.rb, line 208
def decode_octets(base64_encoded_coordinate)
  bytes = ::JWT::Base64.url_decode(base64_encoded_coordinate)
  # Some base64 encoders on some platform omit a single 0-byte at
  # the start of either Y or X coordinate of the elliptic curve point.
  # This leads to an encoding error when data is passed to OpenSSL BN.
  # It is know to have happend to exported JWKs on a Java application and
  # on a Flutter/Dart application (both iOS and Android). All that is
  # needed to fix the problem is adding a leading 0-byte. We know the
  # required byte is 0 because with any other byte the point is no longer
  # on the curve - and OpenSSL will actually communicate this via another
  # exception. The indication of a stripped byte will be the fact that the
  # coordinates - once decoded into bytes - should always be an even
  # bytesize. For example, with a P-521 curve, both x and y must be 66 bytes.
  # With a P-256 curve, both x and y must be 32 and so on. The simplest way
  # to check for this truncation is thus to check whether the number of bytes
  # is odd, and restore the leading 0-byte if it is.
  if bytes.bytesize.odd?
    ZERO_BYTE + bytes
  else
    bytes
  end
end
ec_key() click to toggle source
# File lib/jwt/jwk/ec.rb, line 77
def ec_key
  @ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
end
encode_octets(octets) click to toggle source
# File lib/jwt/jwk/ec.rb, line 122
def encode_octets(octets)
  return unless octets

  ::JWT::Base64.url_encode(octets)
end
encode_open_ssl_bn(key_part) click to toggle source
# File lib/jwt/jwk/ec.rb, line 128
def encode_open_ssl_bn(key_part)
  ::JWT::Base64.url_encode(key_part.to_s(BINARY))
end
extract_key_params(key) click to toggle source
# File lib/jwt/jwk/ec.rb, line 81
def extract_key_params(key)
  case key
  when JWT::JWK::EC
    key.export(include_private: true)
  when OpenSSL::PKey::EC # Accept OpenSSL key as input
    @ec_key = key # Preserve the object to avoid recreation
    parse_ec_key(key)
  when Hash
    key.transform_keys(&:to_sym)
  else
    raise ArgumentError, 'key must be of type OpenSSL::PKey::EC or Hash with key parameters'
  end
end
keypair_components(ec_keypair) click to toggle source
# File lib/jwt/jwk/ec.rb, line 101
def keypair_components(ec_keypair)
  encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
  case ec_keypair.group.curve_name
  when 'prime256v1'
    crv = 'P-256'
    x_octets, y_octets = encoded_point.unpack('xa32a32')
  when 'secp256k1'
    crv = 'P-256K'
    x_octets, y_octets = encoded_point.unpack('xa32a32')
  when 'secp384r1'
    crv = 'P-384'
    x_octets, y_octets = encoded_point.unpack('xa48a48')
  when 'secp521r1'
    crv = 'P-521'
    x_octets, y_octets = encoded_point.unpack('xa66a66')
  else
    raise JWT::JWKError, "Unsupported curve '#{ec_keypair.group.curve_name}'"
  end
  [crv, x_octets, y_octets]
end
parse_ec_key(key) click to toggle source
# File lib/jwt/jwk/ec.rb, line 132
def parse_ec_key(key)
  crv, x_octets, y_octets = keypair_components(key)
  octets = key.private_key&.to_bn&.to_s(BINARY)
  {
    kty: KTY,
    crv: crv,
    x: encode_octets(x_octets),
    y: encode_octets(y_octets),
    d: encode_octets(octets)
  }.compact
end