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.
- checksums.yaml +7 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/actions/install-openssl/action.yml +35 -0
- data/.github/actions/install-ruby/action.yml +76 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/build.yml +49 -0
- data/.github/workflows/git.yml +21 -0
- data/.github/workflows/release.yml +28 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.rubocop.yml +306 -0
- data/CHANGELOG.md +462 -0
- data/CONTRIBUTING.md +39 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +479 -0
- data/Rakefile +10 -0
- data/SECURITY.md +21 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/docs/advanced_configuration.md +173 -0
- data/docs/u2f_migration.md +90 -0
- data/lib/webauthn/attestation_object.rb +47 -0
- data/lib/webauthn/attestation_statement/apple.rb +65 -0
- data/lib/webauthn/attestation_statement/base.rb +175 -0
- data/lib/webauthn/attestation_statement/fido_u2f/public_key.rb +44 -0
- data/lib/webauthn/attestation_statement/fido_u2f.rb +71 -0
- data/lib/webauthn/attestation_statement/none.rb +23 -0
- data/lib/webauthn/attestation_statement/packed.rb +72 -0
- data/lib/webauthn/attestation_statement.rb +35 -0
- data/lib/webauthn/authenticator_assertion_response.rb +87 -0
- data/lib/webauthn/authenticator_attestation_response.rb +84 -0
- data/lib/webauthn/authenticator_data/attested_credential_data.rb +94 -0
- data/lib/webauthn/authenticator_data.rb +130 -0
- data/lib/webauthn/authenticator_response.rb +137 -0
- data/lib/webauthn/cbor.rb +189 -0
- data/lib/webauthn/client_data.rb +67 -0
- data/lib/webauthn/configuration.rb +68 -0
- data/lib/webauthn/cose.rb +237 -0
- data/lib/webauthn/credential.rb +26 -0
- data/lib/webauthn/credential_creation_options.rb +97 -0
- data/lib/webauthn/credential_entity.rb +11 -0
- data/lib/webauthn/credential_options.rb +17 -0
- data/lib/webauthn/credential_request_options.rb +44 -0
- data/lib/webauthn/credential_rp_entity.rb +8 -0
- data/lib/webauthn/credential_user_entity.rb +16 -0
- data/lib/webauthn/encoder.rb +18 -0
- data/lib/webauthn/encoders.rb +62 -0
- data/lib/webauthn/error.rb +5 -0
- data/lib/webauthn/fake_authenticator/attestation_object.rb +90 -0
- data/lib/webauthn/fake_authenticator/authenticator_data.rb +156 -0
- data/lib/webauthn/fake_authenticator.rb +134 -0
- data/lib/webauthn/fake_client.rb +179 -0
- data/lib/webauthn/json_serializer.rb +45 -0
- data/lib/webauthn/public_key.rb +65 -0
- data/lib/webauthn/public_key_credential/creation_options.rb +93 -0
- data/lib/webauthn/public_key_credential/entity.rb +21 -0
- data/lib/webauthn/public_key_credential/options.rb +51 -0
- data/lib/webauthn/public_key_credential/request_options.rb +46 -0
- data/lib/webauthn/public_key_credential/rp_entity.rb +23 -0
- data/lib/webauthn/public_key_credential/user_entity.rb +24 -0
- data/lib/webauthn/public_key_credential.rb +94 -0
- data/lib/webauthn/public_key_credential_with_assertion.rb +49 -0
- data/lib/webauthn/public_key_credential_with_attestation.rb +30 -0
- data/lib/webauthn/relying_party.rb +150 -0
- data/lib/webauthn/u2f_migrator.rb +76 -0
- data/lib/webauthn/version.rb +5 -0
- data/lib/webauthn.rb +19 -0
- data/webauthn.gemspec +43 -0
- 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
|