File: fake_u2f.rb

package info (click to toggle)
ruby-u2f 0.2.1-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 164 kB
  • sloc: ruby: 727; makefile: 12
file content (194 lines) | stat: -rw-r--r-- 5,726 bytes parent folder | download | duplicates (3)
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
class U2F::FakeU2F
  CURVE_NAME   = "prime256v1".freeze

  attr_accessor :app_id, :counter, :key_handle_raw, :cert_subject

  # Initialize a new FakeU2F device for use in tests.
  #
  # app_id  - The appId/origin this is being tested against.
  # options - A Hash of optional parameters (optional).
  #           :counter      - The initial counter for this device.
  #           :key_handle   - The raw key-handle this device should use.
  #           :cert_subject - The subject field for the certificate generated
  #                           for this device.
  #
  # Returns nothing.
  def initialize(app_id, options = {})
    @app_id = app_id
    @counter = options.fetch(:counter, 0)
    @key_handle_raw = options.fetch(:key_handle, SecureRandom.random_bytes(32))
    @cert_subject = options.fetch(:cert_subject, "/CN=U2FTest")
  end

  # A registerResponse hash as returned by the u2f.register JavaScript API.
  #
  # challenge - The challenge to sign.
  # error     - Boolean. Whether to return an error response (optional).
  #
  # Returns a JSON encoded Hash String.
  def register_response(challenge, error = false)
    if error
      JSON.dump(:errorCode => 4)
    else
      client_data_json = client_data(U2F::ClientData::REGISTRATION_TYP, challenge)
      JSON.dump(
        :registrationData => reg_registration_data(client_data_json),
        :clientData => U2F.urlsafe_encode64(client_data_json)
      )
    end
  end

  # A SignResponse hash as returned by the u2f.sign JavaScript API.
  #
  # challenge - The challenge to sign.
  #
  # Returns a JSON encoded Hash String.
  def sign_response(challenge)
    client_data_json = client_data(U2F::ClientData::AUTHENTICATION_TYP, challenge)
    JSON.dump(
      :clientData => U2F.urlsafe_encode64(client_data_json),
      :keyHandle => U2F.urlsafe_encode64(key_handle_raw),
      :signatureData => auth_signature_data(client_data_json)
    )
  end

  # The appId specific public key as returned in the registrationData field of
  # a RegisterResponse Hash.
  #
  # Returns a binary formatted EC public key String.
  def origin_public_key_raw
    [origin_key.public_key.to_bn.to_s(16)].pack('H*')
  end

  # The raw device attestation certificate as returned in the registrationData
  # field of a RegisterResponse Hash.
  #
  # Returns a DER formatted certificate String.
  def cert_raw
    cert.to_der
  end

  private

  # The registrationData field returns in a RegisterResponse Hash.
  #
  # client_data_json - The JSON encoded clientData String.
  #
  # Returns a url-safe base64 encoded binary String.
  def reg_registration_data(client_data_json)
    U2F.urlsafe_encode64(
      [
        5,
        origin_public_key_raw,
        key_handle_raw.bytesize,
        key_handle_raw,
        cert_raw,
        reg_signature(client_data_json)
      ].pack("CA65CA#{key_handle_raw.bytesize}A#{cert_raw.bytesize}A*")
    )
  end

  # The signature field of a registrationData field of a RegisterResponse.
  #
  # client_data_json - The JSON encoded clientData String.
  #
  # Returns an ECDSA signature String.
  def reg_signature(client_data_json)
    payload = [
      "\x00",
      U2F::DIGEST.digest(app_id),
      U2F::DIGEST.digest(client_data_json),
      key_handle_raw,
      origin_public_key_raw
    ].join
    cert_key.sign(U2F::DIGEST.new, payload)
  end

  # The signatureData field of a SignResponse Hash.
  #
  # client_data_json - The JSON encoded clientData String.
  #
  # Returns a url-safe base64 encoded binary String.
  def auth_signature_data(client_data_json)
    ::U2F.urlsafe_encode64(
      [
        1, # User present
        self.counter += 1,
        auth_signature(client_data_json)
      ].pack("CNA*")
    )
  end

  # The signature field of a signatureData field of a SignResponse Hash.
  #
  # client_data_json - The JSON encoded clientData String.
  #
  # Returns an ECDSA signature String.
  def auth_signature(client_data_json)
    data = [
      U2F::DIGEST.digest(app_id),
      1, # User present
      counter,
      U2F::DIGEST.digest(client_data_json)
    ].pack("A32CNA32")

    origin_key.sign(U2F::DIGEST.new, data)
  end

  # The clientData hash as returned by registration and authentication
  # responses.
  #
  # typ       - The String value for the 'typ' field.
  # challenge - The String url-safe base64 encoded challenge parameter.
  #
  # Returns a JSON encoded Hash String.
  def client_data(typ, challenge)
    JSON.dump(
      :challenge => challenge,
      :origin    => app_id,
      :typ       => typ
    )
  end

  # The appId-specific public/private key.
  #
  # Returns a OpenSSL::PKey::EC instance.
  def origin_key
    @origin_key ||= generate_ec_key
  end

  # The self-signed device attestation certificate.
  #
  # Returns a OpenSSL::X509::Certificate instance.
  def cert
    @cert ||= OpenSSL::X509::Certificate.new.tap do |c|
      c.subject = c.issuer = OpenSSL::X509::Name.parse(cert_subject)
      c.not_before = Time.now
      c.not_after = Time.now + 365 * 24 * 60 * 60
      c.public_key = cert_key
      c.serial = 0x1
      c.version = 0x0
      c.sign cert_key, U2F::DIGEST.new
    end
  end

  # The public key used for signing the device certificate.
  #
  # Returns a OpenSSL::PKey::EC instance.
  def cert_key
    @cert_key ||= generate_ec_key
  end

  # Generate an eliptic curve public/private key.
  #
  # Returns a OpenSSL::PKey::EC instance.
  def generate_ec_key
    OpenSSL::PKey::EC.new().tap do |ec|
      ec.group = OpenSSL::PKey::EC::Group.new(CURVE_NAME)
      ec.generate_key
      # https://bugs.ruby-lang.org/issues/8177
      ec.define_singleton_method(:private?) { private_key? }
      ec.define_singleton_method(:public?) { public_key? }
    end
  end
end