ecdsa.rb 2.37 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
class Acme::Client::JWK::ECDSA < Acme::Client::JWK::Base
  # JWA parameters for supported OpenSSL curves.
  # https://tools.ietf.org/html/rfc7518#section-3.1
  KNOWN_CURVES = {
    'prime256v1' => {
      jwa_crv: 'P-256',
      jwa_alg: 'ES256',
      digest: OpenSSL::Digest::SHA256
    }.freeze,
    'secp384r1' => {
      jwa_crv: 'P-384',
      jwa_alg: 'ES384',
      digest: OpenSSL::Digest::SHA384
    }.freeze,
    'secp521r1' => {
      jwa_crv: 'P-521',
      jwa_alg: 'ES512',
      digest: OpenSSL::Digest::SHA512
    }.freeze
  }.freeze

  # Instantiate a new ECDSA JWK.
  #
  # private_key - A OpenSSL::PKey::EC instance.
  #
  # Returns nothing.
  def initialize(private_key)
    unless private_key.is_a?(OpenSSL::PKey::EC)
      raise ArgumentError, 'private_key must be a OpenSSL::PKey::EC'
    end

    unless @curve_params = KNOWN_CURVES[private_key.group.curve_name]
      raise ArgumentError, 'Unknown EC curve'
    end

    @private_key = private_key
  end

  # The name of the algorithm as needed for the `alg` member of a JWS object.
  #
  # Returns a String.
  def jwa_alg
    @curve_params[:jwa_alg]
  end

  # Get this JWK as a Hash for JSON serialization.
  #
  # Returns a Hash.
  def to_h
    {
      crv: @curve_params[:jwa_crv],
      kty: 'EC',
      x: Acme::Client::Util.urlsafe_base64(coordinates[:x].to_s(2)),
      y: Acme::Client::Util.urlsafe_base64(coordinates[:y].to_s(2))
    }
  end

  # Sign a message with the private key.
  #
  # message - A String message to sign.
  #
  # Returns a String signature.
  def sign(message)
    # DER encoded ASN.1 signature
    der = @private_key.sign(@curve_params[:digest].new, message)

    # ASN.1 SEQUENCE
    seq = OpenSSL::ASN1.decode(der)

    # ASN.1 INTs
    ints = seq.value

    # BigNumbers
    bns = ints.map(&:value)

    # Binary R/S values
    r, s = bns.map { |bn| [bn.to_s(16)].pack('H*') }

    # JWS wants raw R/S concatenated.
    [r, s].join
  end

  private

  # rubocop:disable Metrics/AbcSize
  def coordinates
    @coordinates ||= begin
      hex = public_key.to_bn.to_s(16)
      data_len = hex.length - 2
      hex_x = hex[2, data_len / 2]
      hex_y = hex[2 + data_len / 2, data_len / 2]

      {
        x: OpenSSL::BN.new([hex_x].pack('H*'), 2),
        y: OpenSSL::BN.new([hex_y].pack('H*'), 2)
      }
    end
  end
  # rubocop:enable Metrics/AbcSize

  def public_key
    @private_key.public_key
  end
end