webauthn-slim 3.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/actions/install-openssl/action.yml +35 -0
  4. data/.github/actions/install-ruby/action.yml +76 -0
  5. data/.github/dependabot.yml +6 -0
  6. data/.github/workflows/build.yml +49 -0
  7. data/.github/workflows/git.yml +21 -0
  8. data/.github/workflows/release.yml +28 -0
  9. data/.gitignore +16 -0
  10. data/.rspec +2 -0
  11. data/.rubocop.yml +306 -0
  12. data/CHANGELOG.md +462 -0
  13. data/CONTRIBUTING.md +39 -0
  14. data/Gemfile +8 -0
  15. data/LICENSE.txt +21 -0
  16. data/README.md +479 -0
  17. data/Rakefile +10 -0
  18. data/SECURITY.md +21 -0
  19. data/bin/console +15 -0
  20. data/bin/setup +8 -0
  21. data/docs/advanced_configuration.md +173 -0
  22. data/docs/u2f_migration.md +90 -0
  23. data/lib/webauthn/attestation_object.rb +47 -0
  24. data/lib/webauthn/attestation_statement/apple.rb +65 -0
  25. data/lib/webauthn/attestation_statement/base.rb +175 -0
  26. data/lib/webauthn/attestation_statement/fido_u2f/public_key.rb +44 -0
  27. data/lib/webauthn/attestation_statement/fido_u2f.rb +71 -0
  28. data/lib/webauthn/attestation_statement/none.rb +23 -0
  29. data/lib/webauthn/attestation_statement/packed.rb +72 -0
  30. data/lib/webauthn/attestation_statement.rb +35 -0
  31. data/lib/webauthn/authenticator_assertion_response.rb +87 -0
  32. data/lib/webauthn/authenticator_attestation_response.rb +84 -0
  33. data/lib/webauthn/authenticator_data/attested_credential_data.rb +94 -0
  34. data/lib/webauthn/authenticator_data.rb +130 -0
  35. data/lib/webauthn/authenticator_response.rb +137 -0
  36. data/lib/webauthn/cbor.rb +189 -0
  37. data/lib/webauthn/client_data.rb +67 -0
  38. data/lib/webauthn/configuration.rb +68 -0
  39. data/lib/webauthn/cose.rb +237 -0
  40. data/lib/webauthn/credential.rb +26 -0
  41. data/lib/webauthn/credential_creation_options.rb +97 -0
  42. data/lib/webauthn/credential_entity.rb +11 -0
  43. data/lib/webauthn/credential_options.rb +17 -0
  44. data/lib/webauthn/credential_request_options.rb +44 -0
  45. data/lib/webauthn/credential_rp_entity.rb +8 -0
  46. data/lib/webauthn/credential_user_entity.rb +16 -0
  47. data/lib/webauthn/encoder.rb +18 -0
  48. data/lib/webauthn/encoders.rb +62 -0
  49. data/lib/webauthn/error.rb +5 -0
  50. data/lib/webauthn/fake_authenticator/attestation_object.rb +90 -0
  51. data/lib/webauthn/fake_authenticator/authenticator_data.rb +156 -0
  52. data/lib/webauthn/fake_authenticator.rb +134 -0
  53. data/lib/webauthn/fake_client.rb +179 -0
  54. data/lib/webauthn/json_serializer.rb +45 -0
  55. data/lib/webauthn/public_key.rb +65 -0
  56. data/lib/webauthn/public_key_credential/creation_options.rb +93 -0
  57. data/lib/webauthn/public_key_credential/entity.rb +21 -0
  58. data/lib/webauthn/public_key_credential/options.rb +51 -0
  59. data/lib/webauthn/public_key_credential/request_options.rb +46 -0
  60. data/lib/webauthn/public_key_credential/rp_entity.rb +23 -0
  61. data/lib/webauthn/public_key_credential/user_entity.rb +24 -0
  62. data/lib/webauthn/public_key_credential.rb +94 -0
  63. data/lib/webauthn/public_key_credential_with_assertion.rb +49 -0
  64. data/lib/webauthn/public_key_credential_with_attestation.rb +30 -0
  65. data/lib/webauthn/relying_party.rb +150 -0
  66. data/lib/webauthn/u2f_migrator.rb +76 -0
  67. data/lib/webauthn/version.rb +5 -0
  68. data/lib/webauthn.rb +19 -0
  69. data/webauthn.gemspec +43 -0
  70. metadata +218 -0
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/error"
4
+
5
+ module WebAuthn
6
+ # The WebAuthn subset of RFC 8949, without a native extension. Inspired by
7
+ # the bounded decoder in https://github.com/rails/rails/pull/57100.
8
+ module CBOR
9
+ class MalformedData < WebAuthn::Error; end
10
+
11
+ MAX_SIZE = 10 * 1024 * 1024
12
+ MAX_DEPTH = 16
13
+
14
+ def self.decode(data)
15
+ decoder = Decoder.new(data)
16
+ value = decoder.decode
17
+ raise MalformedData, "Trailing CBOR data" unless decoder.position == data.bytesize
18
+
19
+ value
20
+ end
21
+
22
+ # Returns both the first item and its exact encoded length. Credential keys
23
+ # can be followed by extensions, and need not use the shortest encoding.
24
+ def self.decode_first(data)
25
+ decoder = Decoder.new(data)
26
+ [decoder.decode, decoder.position]
27
+ end
28
+
29
+ def self.encode(value)
30
+ case value
31
+ when Integer
32
+ value >= 0 ? header(0, value) : header(1, -1 - value)
33
+ when String
34
+ header(value.encoding == Encoding::BINARY ? 2 : 3, value.bytesize) + value.b
35
+ when Array
36
+ header(4, value.length) + value.map { |item| encode(item) }.join
37
+ when Hash
38
+ header(5, value.length) + value.map { |key, item| encode(key) + encode(item) }.join
39
+ when false then "\xf4".b
40
+ when true then "\xf5".b
41
+ when nil then "\xf6".b
42
+ when Float then "\xfb".b + [value].pack("G")
43
+ else raise ArgumentError, "Unsupported CBOR value #{value.class}"
44
+ end
45
+ end
46
+
47
+ def self.header(type, value)
48
+ prefix = type << 5
49
+ case value
50
+ when 0..23 then [prefix | value].pack("C")
51
+ when 24..255 then [prefix | 24, value].pack("CC")
52
+ when 256..65535 then [prefix | 25, value].pack("Cn")
53
+ when 65536..0xffffffff then [prefix | 26, value].pack("CN")
54
+ when 0x100000000..0xffffffffffffffff then [prefix | 27, value].pack("CQ>")
55
+ else raise ArgumentError, "CBOR integer out of range"
56
+ end
57
+ end
58
+ private_class_method :header
59
+
60
+ class Decoder
61
+ attr_reader :position
62
+
63
+ def initialize(data)
64
+ raise MalformedData, "Expected CBOR bytes" unless data.is_a?(String)
65
+ raise MalformedData, "Input exceeds maximum size" if data.bytesize > MAX_SIZE
66
+
67
+ @data = data.b
68
+ @position = 0
69
+ end
70
+
71
+ def decode(depth = 0)
72
+ raise MalformedData, "Maximum nesting depth exceeded" if depth >= MAX_DEPTH
73
+
74
+ byte = read(1).getbyte(0)
75
+ type = byte >> 5
76
+ info = byte & 31
77
+ return simple(info) if type == 7
78
+
79
+ if info == 31
80
+ return indefinite(type, depth)
81
+ end
82
+
83
+ size = argument(info)
84
+ case type
85
+ when 0 then size
86
+ when 1 then -1 - size
87
+ when 2, 3 then string(type, read(size))
88
+ when 4
89
+ raise MalformedData, "Truncated CBOR array" if size > remaining
90
+
91
+ Array.new(size) { decode(depth + 1) }
92
+ when 5
93
+ raise MalformedData, "Truncated CBOR map" if size > remaining / 2
94
+
95
+ {}.tap { |map| size.times { read_pair(map, depth) } }
96
+ else raise MalformedData, "Unsupported CBOR tag #{size}"
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def read(size)
103
+ raise MalformedData, "Unexpected end of input" if size > remaining
104
+
105
+ bytes = @data.byteslice(@position, size)
106
+ @position += size
107
+ bytes
108
+ end
109
+
110
+ def remaining
111
+ @data.bytesize - position
112
+ end
113
+
114
+ def argument(info)
115
+ case info
116
+ when 0..23 then info
117
+ when 24 then read(1).unpack1("C")
118
+ when 25 then read(2).unpack1("n")
119
+ when 26 then read(4).unpack1("N")
120
+ when 27 then read(8).unpack1("Q>")
121
+ else raise MalformedData, "Invalid CBOR additional information"
122
+ end
123
+ end
124
+
125
+ def string(type, bytes)
126
+ return bytes if type == 2
127
+
128
+ bytes.force_encoding(Encoding::UTF_8)
129
+ raise MalformedData, "Invalid CBOR UTF-8 string" unless bytes.valid_encoding?
130
+
131
+ bytes
132
+ end
133
+
134
+ def read_pair(map, depth)
135
+ key = decode(depth + 1)
136
+ raise MalformedData, "Duplicate CBOR map key" if map.key?(key)
137
+
138
+ map[key] = decode(depth + 1)
139
+ end
140
+
141
+ def indefinite(type, depth)
142
+ raise MalformedData, "Invalid indefinite CBOR item" unless (2..5).include?(type)
143
+
144
+ value = type == 5 ? {} : (type == 4 ? [] : "".b)
145
+ until @data.getbyte(position) == 255
146
+ if type == 5
147
+ read_pair(value, depth)
148
+ elsif type == 4
149
+ value << decode(depth + 1)
150
+ else
151
+ byte = @data.getbyte(position)
152
+ unless byte && byte >> 5 == type && byte & 31 != 31
153
+ raise MalformedData, "Invalid indefinite string chunk"
154
+ end
155
+
156
+ value << decode(depth + 1)
157
+ end
158
+ end
159
+ read(1)
160
+ type == 3 ? string(type, value) : value
161
+ end
162
+
163
+ def simple(info)
164
+ case info
165
+ when 20 then false
166
+ when 21 then true
167
+ when 22, 23 then nil
168
+ when 25
169
+ half = read(2).unpack1("n")
170
+ exponent = (half >> 10) & 31
171
+ mantissa = half & 1023
172
+ value =
173
+ if exponent == 0
174
+ Math.ldexp(mantissa, -24)
175
+ elsif exponent == 31
176
+ mantissa == 0 ? Float::INFINITY : Float::NAN
177
+ else
178
+ Math.ldexp(mantissa + 1024, exponent - 25)
179
+ end
180
+ half & 0x8000 == 0 ? value : -value
181
+ when 26 then read(4).unpack1("g")
182
+ when 27 then read(8).unpack1("G")
183
+ else raise MalformedData, "Unsupported CBOR simple value"
184
+ end
185
+ end
186
+ end
187
+ private_constant :Decoder
188
+ end
189
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "openssl"
5
+ require "webauthn/encoder"
6
+ require "webauthn/error"
7
+
8
+ module WebAuthn
9
+ class ClientDataMissingError < Error; end
10
+
11
+ class ClientData
12
+ VALID_TOKEN_BINDING_STATUSES = ["present", "supported", "not-supported"].freeze
13
+
14
+ def initialize(client_data_json)
15
+ @client_data_json = client_data_json
16
+ end
17
+
18
+ def type
19
+ data["type"]
20
+ end
21
+
22
+ def challenge
23
+ WebAuthn.standard_encoder.decode(data["challenge"])
24
+ end
25
+
26
+ def origin
27
+ data["origin"]
28
+ end
29
+
30
+ def token_binding
31
+ data["tokenBinding"]
32
+ end
33
+
34
+ def cross_origin
35
+ data["crossOrigin"]
36
+ end
37
+
38
+ def top_origin
39
+ data["topOrigin"]
40
+ end
41
+
42
+ def valid_token_binding_format?
43
+ if token_binding
44
+ token_binding.is_a?(Hash) && VALID_TOKEN_BINDING_STATUSES.include?(token_binding["status"])
45
+ else
46
+ true
47
+ end
48
+ end
49
+
50
+ def hash
51
+ OpenSSL::Digest::SHA256.digest(client_data_json)
52
+ end
53
+
54
+ private
55
+
56
+ attr_reader :client_data_json
57
+
58
+ def data
59
+ @data ||=
60
+ if client_data_json
61
+ JSON.parse(client_data_json)
62
+ else
63
+ raise ClientDataMissingError, "Client Data JSON is missing"
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'webauthn/relying_party'
5
+
6
+ module WebAuthn
7
+ def self.configuration
8
+ @configuration ||= Configuration.new
9
+ end
10
+
11
+ def self.configure
12
+ yield(configuration)
13
+ end
14
+
15
+ class Configuration
16
+ extend Forwardable
17
+
18
+ def_delegators :@relying_party,
19
+ :algorithms,
20
+ :algorithms=,
21
+ :encoding,
22
+ :encoding=,
23
+ :origin,
24
+ :origin=,
25
+ :allowed_origins,
26
+ :allowed_origins=,
27
+ :allowed_top_origins,
28
+ :allowed_top_origins=,
29
+ :verify_attestation_statement,
30
+ :verify_attestation_statement=,
31
+ :verify_cross_origin,
32
+ :verify_cross_origin=,
33
+ :credential_options_timeout,
34
+ :credential_options_timeout=,
35
+ :silent_authentication,
36
+ :silent_authentication=,
37
+ :acceptable_attestation_types,
38
+ :acceptable_attestation_types=,
39
+ :attestation_root_certificates_finders,
40
+ :attestation_root_certificates_finders=,
41
+ :encoder,
42
+ :encoder=,
43
+ :legacy_u2f_appid,
44
+ :legacy_u2f_appid=
45
+
46
+ attr_reader :relying_party
47
+
48
+ def initialize
49
+ @relying_party = RelyingParty.new
50
+ end
51
+
52
+ def rp_name
53
+ relying_party.name
54
+ end
55
+
56
+ def rp_name=(name)
57
+ relying_party.name = name
58
+ end
59
+
60
+ def rp_id
61
+ relying_party.id
62
+ end
63
+
64
+ def rp_id=(id)
65
+ relying_party.id = id
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "webauthn/cbor"
5
+
6
+ module WebAuthn
7
+ # Public COSE keys and signature algorithms used by WebAuthn (RFC 9053).
8
+ # SubjectPublicKeyInfo construction follows the approach in Rails PR #57100.
9
+ module COSE
10
+ class Error < WebAuthn::Error; end
11
+ class MalformedKeyError < Error; end
12
+ class UnknownKeyType < Error; end
13
+
14
+ module Key
15
+ Curve =
16
+ Struct.new(:id, :name, :pkey_name, :coordinate_length) do
17
+ def self.find(id)
18
+ CURVES.find { |curve| curve.id == id }
19
+ end
20
+
21
+ def self.by_name(name)
22
+ CURVES.find { |curve| curve.name == name }
23
+ end
24
+
25
+ def self.by_pkey_name(name)
26
+ CURVES.find { |curve| curve.pkey_name == name }
27
+ end
28
+ end
29
+
30
+ CURVES = [
31
+ Curve.new(1, "P-256", "prime256v1", 32),
32
+ Curve.new(2, "P-384", "secp384r1", 48),
33
+ Curve.new(3, "P-521", "secp521r1", 66),
34
+ Curve.new(6, "Ed25519", "ED25519", 32),
35
+ Curve.new(7, "Ed448", "ED448", 57),
36
+ Curve.new(8, "secp256k1", "secp256k1", 32)
37
+ ].each(&:freeze).freeze
38
+
39
+ def self.deserialize(bytes)
40
+ map = CBOR.decode(bytes)
41
+ type = map.is_a?(Hash) && map[1]
42
+ case type
43
+ when 1 then OKP.new(alg: map[3], crv: map[-1], x: map[-2])
44
+ when 2 then EC2.new(alg: map[3], crv: map[-1], x: map[-2], y: map[-3])
45
+ when 3 then RSA.new(alg: map[3], n: map[-1], e: map[-2])
46
+ else raise UnknownKeyType, "Unsupported COSE key type #{type}"
47
+ end
48
+ rescue CBOR::MalformedData, ArgumentError => e
49
+ raise MalformedKeyError, e.message
50
+ end
51
+
52
+ class Base
53
+ attr_accessor :alg
54
+
55
+ def self.deserialize(bytes)
56
+ key = Key.deserialize(bytes)
57
+ raise MalformedKeyError, "Unexpected COSE key type" unless key.is_a?(self)
58
+
59
+ key
60
+ end
61
+
62
+ def initialize(alg: nil)
63
+ raise ArgumentError, "Invalid COSE algorithm" unless alg.nil? || alg.is_a?(Integer)
64
+
65
+ @alg = alg
66
+ end
67
+
68
+ def serialize
69
+ CBOR.encode(map)
70
+ end
71
+
72
+ private
73
+
74
+ def bytes?(value, length = nil)
75
+ value.is_a?(String) && value.encoding == Encoding::BINARY &&
76
+ (length ? value.bytesize == length : !value.empty?)
77
+ end
78
+
79
+ def subject_public_key(identifiers, bytes)
80
+ OpenSSL::PKey.read(
81
+ OpenSSL::ASN1::Sequence(
82
+ [
83
+ OpenSSL::ASN1::Sequence(identifiers.map { |name| OpenSSL::ASN1::ObjectId(name) }),
84
+ OpenSSL::ASN1::BitString(bytes)
85
+ ]
86
+ ).to_der
87
+ )
88
+ end
89
+ end
90
+
91
+ class EC2 < Base
92
+ attr_reader :crv, :x, :y
93
+
94
+ def self.from_pkey(key)
95
+ curve = Curve.by_pkey_name(key.group.curve_name)
96
+ raise ArgumentError, "Unsupported EC curve" unless curve
97
+
98
+ point = key.is_a?(OpenSSL::PKey::EC) ? key.public_key : key
99
+ bytes = point.to_octet_string(:uncompressed)
100
+ size = curve.coordinate_length
101
+ new(crv: curve.id, x: bytes.byteslice(1, size), y: bytes.byteslice(1 + size, size))
102
+ end
103
+
104
+ def initialize(crv:, x:, y:, **options) # rubocop:disable Naming/MethodParameterName
105
+ super(**options)
106
+ @crv, @x, @y = crv, x, y
107
+ curve = Curve.find(crv)
108
+ unless [1, 2, 3, 8].include?(crv) && bytes?(x, curve.coordinate_length) && bytes?(y, curve.coordinate_length)
109
+ raise ArgumentError, "Invalid EC2 curve or coordinates"
110
+ end
111
+ end
112
+
113
+ def map
114
+ { 1 => 2, 3 => alg, -1 => crv, -2 => x, -3 => y }.compact
115
+ end
116
+
117
+ def to_pkey
118
+ subject_public_key(["id-ecPublicKey", Curve.find(crv).pkey_name], "\x04".b + x + y)
119
+ end
120
+ end
121
+
122
+ class OKP < Base
123
+ attr_reader :crv, :x
124
+
125
+ def self.from_pkey(key)
126
+ curve = Curve.by_pkey_name(key.oid)
127
+ raise ArgumentError, "Unsupported OKP curve" unless curve
128
+
129
+ new(crv: curve.id, x: OpenSSL::ASN1.decode(key.public_to_der).value.last.value)
130
+ end
131
+
132
+ def initialize(crv:, x:, **options) # rubocop:disable Naming/MethodParameterName
133
+ super(**options)
134
+ @crv, @x = crv, x
135
+ unless [6, 7].include?(crv) && bytes?(x, Curve.find(crv).coordinate_length)
136
+ raise ArgumentError, "Invalid OKP curve or public key"
137
+ end
138
+ end
139
+
140
+ def map
141
+ { 1 => 1, 3 => alg, -1 => crv, -2 => x }.compact
142
+ end
143
+
144
+ def to_pkey
145
+ subject_public_key([Curve.find(crv).pkey_name], x)
146
+ end
147
+ end
148
+
149
+ class RSA < Base
150
+ attr_reader :n, :e
151
+
152
+ def self.from_pkey(key)
153
+ new(n: key.n.to_s(2), e: key.e.to_s(2))
154
+ end
155
+
156
+ def initialize(n:, e:, **options) # rubocop:disable Naming/MethodParameterName
157
+ super(**options)
158
+ raise ArgumentError, "Invalid RSA modulus or exponent" unless bytes?(n) && bytes?(e)
159
+
160
+ @n, @e = n, e
161
+ end
162
+
163
+ def map
164
+ { 1 => 3, 3 => alg, -1 => n, -2 => e }.compact
165
+ end
166
+
167
+ def to_pkey
168
+ OpenSSL::PKey::RSA.new(
169
+ OpenSSL::ASN1::Sequence([n, e].map { |bytes| OpenSSL::ASN1::Integer(OpenSSL::BN.new(bytes, 2)) }).to_der
170
+ )
171
+ end
172
+ end
173
+ end
174
+
175
+ Algorithm =
176
+ Struct.new(:id, :name, :hash_function, :key_type, :curve_name) do
177
+ def self.find(id_or_name)
178
+ ALGORITHMS.find { |algorithm| algorithm.id == id_or_name || algorithm.name == id_or_name }
179
+ end
180
+
181
+ def self.by_name(name)
182
+ find(name)
183
+ end
184
+
185
+ def compatible_key?(key)
186
+ case key_type
187
+ when :ec
188
+ key.is_a?(OpenSSL::PKey::EC) && key.group.curve_name == curve_name
189
+ when :rsa
190
+ key.is_a?(OpenSSL::PKey::RSA)
191
+ when :eddsa
192
+ key.respond_to?(:oid) && ["ED25519", "ED448"].include?(key.oid)
193
+ end
194
+ end
195
+
196
+ def verify(key, signature, data)
197
+ return false unless compatible_key?(key)
198
+
199
+ if name.start_with?("PS")
200
+ key.verify_pss(hash_function, signature, data, salt_length: :digest, mgf1_hash: hash_function)
201
+ else
202
+ if key_type == :ec
203
+ size = (key.group.degree + 7) / 8
204
+ if signature.bytesize == size * 2
205
+ signature = OpenSSL::ASN1::Sequence(
206
+ [signature.byteslice(0, size), signature.byteslice(size, size)].map do |bytes|
207
+ OpenSSL::ASN1::Integer(OpenSSL::BN.new(bytes, 2))
208
+ end
209
+ ).to_der
210
+ end
211
+ end
212
+
213
+ key.verify(hash_function, signature, data)
214
+ end
215
+ rescue OpenSSL::PKey::PKeyError
216
+ raise unless key_type == :eddsa
217
+
218
+ false
219
+ end
220
+ end
221
+
222
+ ALGORITHMS = [
223
+ Algorithm.new(-7, "ES256", "SHA256", :ec, "prime256v1"),
224
+ Algorithm.new(-35, "ES384", "SHA384", :ec, "secp384r1"),
225
+ Algorithm.new(-36, "ES512", "SHA512", :ec, "secp521r1"),
226
+ Algorithm.new(-47, "ES256K", "SHA256", :ec, "secp256k1"),
227
+ Algorithm.new(-8, "EdDSA", nil, :eddsa),
228
+ Algorithm.new(-37, "PS256", "SHA256", :rsa),
229
+ Algorithm.new(-38, "PS384", "SHA384", :rsa),
230
+ Algorithm.new(-39, "PS512", "SHA512", :rsa),
231
+ Algorithm.new(-257, "RS256", "SHA256", :rsa),
232
+ Algorithm.new(-258, "RS384", "SHA384", :rsa),
233
+ Algorithm.new(-259, "RS512", "SHA512", :rsa),
234
+ Algorithm.new(-65535, "RS1", "SHA1", :rsa)
235
+ ].each(&:freeze).freeze
236
+ end
237
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/public_key_credential/creation_options"
4
+ require "webauthn/public_key_credential/request_options"
5
+ require "webauthn/public_key_credential_with_assertion"
6
+ require "webauthn/public_key_credential_with_attestation"
7
+
8
+ module WebAuthn
9
+ module Credential
10
+ def self.options_for_create(**keyword_arguments)
11
+ WebAuthn::PublicKeyCredential::CreationOptions.new(**keyword_arguments)
12
+ end
13
+
14
+ def self.options_for_get(**keyword_arguments)
15
+ WebAuthn::PublicKeyCredential::RequestOptions.new(**keyword_arguments)
16
+ end
17
+
18
+ def self.from_create(credential, relying_party: WebAuthn.configuration.relying_party)
19
+ WebAuthn::PublicKeyCredentialWithAttestation.from_client(credential, relying_party: relying_party)
20
+ end
21
+
22
+ def self.from_get(credential, relying_party: WebAuthn.configuration.relying_party)
23
+ WebAuthn::PublicKeyCredentialWithAssertion.from_client(credential, relying_party: relying_party)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/cose"
4
+ require "webauthn/credential_options"
5
+ require "webauthn/credential_rp_entity"
6
+ require "webauthn/credential_user_entity"
7
+
8
+ module WebAuthn
9
+ def self.credential_creation_options(rp_name: nil, user_name: "web-user", display_name: "web-user", user_id: "1")
10
+ warn(
11
+ "DEPRECATION WARNING: `WebAuthn.credential_creation_options` is deprecated."\
12
+ " Please use `WebAuthn::Credential.options_for_create` instead."
13
+ )
14
+
15
+ CredentialCreationOptions.new(
16
+ rp_name: rp_name, user_id: user_id, user_name: user_name, user_display_name: display_name
17
+ ).to_h
18
+ end
19
+
20
+ class CredentialCreationOptions < CredentialOptions
21
+ DEFAULT_RP_NAME = "web-server"
22
+
23
+ attr_accessor :attestation, :authenticator_selection, :exclude_credentials, :extensions
24
+
25
+ def initialize(
26
+ attestation: nil,
27
+ authenticator_selection: nil,
28
+ exclude_credentials: nil,
29
+ extensions: nil,
30
+ user_id:,
31
+ user_name:,
32
+ user_display_name: nil,
33
+ rp_name: nil
34
+ )
35
+ super()
36
+
37
+ @attestation = attestation
38
+ @authenticator_selection = authenticator_selection
39
+ @exclude_credentials = exclude_credentials
40
+ @extensions = extensions
41
+ @user_id = user_id
42
+ @user_name = user_name
43
+ @user_display_name = user_display_name
44
+ @rp_name = rp_name
45
+ end
46
+
47
+ def to_h
48
+ options = {
49
+ challenge: challenge,
50
+ pubKeyCredParams: pub_key_cred_params,
51
+ timeout: timeout,
52
+ user: { id: user.id, name: user.name, displayName: user.display_name },
53
+ rp: { name: rp.name }
54
+ }
55
+
56
+ if attestation
57
+ options[:attestation] = attestation
58
+ end
59
+
60
+ if authenticator_selection
61
+ options[:authenticatorSelection] = authenticator_selection
62
+ end
63
+
64
+ if exclude_credentials
65
+ options[:excludeCredentials] = exclude_credentials
66
+ end
67
+
68
+ if extensions
69
+ options[:extensions] = extensions
70
+ end
71
+
72
+ options
73
+ end
74
+
75
+ def pub_key_cred_params
76
+ configuration.algorithms.map do |alg_name|
77
+ { type: "public-key", alg: COSE::Algorithm.by_name(alg_name).id }
78
+ end
79
+ end
80
+
81
+ def rp
82
+ @rp ||= CredentialRPEntity.new(name: rp_name || configuration.rp_name || DEFAULT_RP_NAME)
83
+ end
84
+
85
+ def user
86
+ @user ||= CredentialUserEntity.new(id: user_id, name: user_name, display_name: user_display_name)
87
+ end
88
+
89
+ private
90
+
91
+ attr_reader :user_id, :user_name, :user_display_name, :rp_name
92
+
93
+ def configuration
94
+ WebAuthn.configuration
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebAuthn
4
+ class CredentialEntity
5
+ attr_reader :name
6
+
7
+ def initialize(name:)
8
+ @name = name
9
+ end
10
+ end
11
+ end