File: fake_client.rb

package info (click to toggle)
ruby-webauthn 2.5.2-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 384 kB
  • sloc: ruby: 2,138; sh: 4; makefile: 4
file content (154 lines) | stat: -rw-r--r-- 4,110 bytes parent folder | download
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
# frozen_string_literal: true

require "openssl"
require "securerandom"
require "webauthn/authenticator_data"
require "webauthn/encoder"
require "webauthn/fake_authenticator"

module WebAuthn
  class FakeClient
    TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze

    attr_reader :origin, :token_binding

    def initialize(
      origin = fake_origin,
      token_binding: nil,
      authenticator: WebAuthn::FakeAuthenticator.new,
      encoding: WebAuthn.configuration.encoding
    )
      @origin = origin
      @token_binding = token_binding
      @authenticator = authenticator
      @encoding = encoding
    end

    def create(
      challenge: fake_challenge,
      rp_id: nil,
      user_present: true,
      user_verified: false,
      attested_credential_data: true,
      extensions: nil
    )
      rp_id ||= URI.parse(origin).host

      client_data_json = data_json_for(:create, encoder.decode(challenge))
      client_data_hash = hashed(client_data_json)

      attestation_object = authenticator.make_credential(
        rp_id: rp_id,
        client_data_hash: client_data_hash,
        user_present: user_present,
        user_verified: user_verified,
        attested_credential_data: attested_credential_data,
        extensions: extensions
      )

      id =
        if attested_credential_data
          WebAuthn::AuthenticatorData
            .deserialize(CBOR.decode(attestation_object)["authData"])
            .attested_credential_data
            .id
        else
          "id-for-pk-without-attested-credential-data"
        end

      {
        "type" => "public-key",
        "id" => internal_encoder.encode(id),
        "rawId" => encoder.encode(id),
        "clientExtensionResults" => extensions,
        "response" => {
          "attestationObject" => encoder.encode(attestation_object),
          "clientDataJSON" => encoder.encode(client_data_json)
        }
      }
    end

    def get(challenge: fake_challenge,
            rp_id: nil,
            user_present: true,
            user_verified: false,
            sign_count: nil,
            extensions: nil,
            user_handle: nil,
            allow_credentials: nil)
      rp_id ||= URI.parse(origin).host

      client_data_json = data_json_for(:get, encoder.decode(challenge))
      client_data_hash = hashed(client_data_json)

      if allow_credentials
        allow_credentials = allow_credentials.map { |credential| encoder.decode(credential) }
      end

      assertion = authenticator.get_assertion(
        rp_id: rp_id,
        client_data_hash: client_data_hash,
        user_present: user_present,
        user_verified: user_verified,
        sign_count: sign_count,
        extensions: extensions,
        allow_credentials: allow_credentials
      )

      {
        "type" => "public-key",
        "id" => internal_encoder.encode(assertion[:credential_id]),
        "rawId" => encoder.encode(assertion[:credential_id]),
        "clientExtensionResults" => extensions,
        "response" => {
          "clientDataJSON" => encoder.encode(client_data_json),
          "authenticatorData" => encoder.encode(assertion[:authenticator_data]),
          "signature" => encoder.encode(assertion[:signature]),
          "userHandle" => user_handle ? encoder.encode(user_handle) : nil
        }
      }
    end

    private

    attr_reader :authenticator, :encoding

    def data_json_for(method, challenge)
      data = {
        type: type_for(method),
        challenge: internal_encoder.encode(challenge),
        origin: origin
      }

      if token_binding
        data[:tokenBinding] = token_binding
      end

      data.to_json
    end

    def encoder
      @encoder ||= WebAuthn::Encoder.new(encoding)
    end

    def internal_encoder
      WebAuthn.standard_encoder
    end

    def hashed(data)
      OpenSSL::Digest::SHA256.digest(data)
    end

    def fake_challenge
      encoder.encode(SecureRandom.random_bytes(32))
    end

    def fake_origin
      "http://localhost#{rand(1000)}.test"
    end

    def type_for(method)
      TYPES[method]
    end
  end
end