smart-id-ruby-client 0.1.0
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/.rspec +3 -0
- data/.rubocop.yml +14 -0
- data/CHANGELOG.md +13 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +436 -0
- data/Rakefile +12 -0
- data/lib/smart-id-ruby-client.rb +3 -0
- data/lib/smart_id_ruby/callback_url.rb +18 -0
- data/lib/smart_id_ruby/callback_url_util.rb +54 -0
- data/lib/smart_id_ruby/client.rb +124 -0
- data/lib/smart_id_ruby/configuration.rb +184 -0
- data/lib/smart_id_ruby/device_link_builder.rb +301 -0
- data/lib/smart_id_ruby/device_link_interaction.rb +67 -0
- data/lib/smart_id_ruby/errors/certificate_level_mismatch_error.rb +8 -0
- data/lib/smart_id_ruby/errors/document_unusable_error.rb +12 -0
- data/lib/smart_id_ruby/errors/error.rb +8 -0
- data/lib/smart_id_ruby/errors/expected_linked_session_error.rb +15 -0
- data/lib/smart_id_ruby/errors/no_suitable_account_of_requested_type_found_error.rb +10 -0
- data/lib/smart_id_ruby/errors/person_should_view_smart_id_portal_error.rb +8 -0
- data/lib/smart_id_ruby/errors/protocol_failure_error.rb +13 -0
- data/lib/smart_id_ruby/errors/relying_party_account_configuration_error.rb +10 -0
- data/lib/smart_id_ruby/errors/request_setup_error.rb +10 -0
- data/lib/smart_id_ruby/errors/request_validation_error.rb +8 -0
- data/lib/smart_id_ruby/errors/required_interaction_not_supported_by_app_error.rb +13 -0
- data/lib/smart_id_ruby/errors/response_error.rb +8 -0
- data/lib/smart_id_ruby/errors/server_maintenance_error.rb +8 -0
- data/lib/smart_id_ruby/errors/session_end_result_error.rb +15 -0
- data/lib/smart_id_ruby/errors/session_not_complete_error.rb +8 -0
- data/lib/smart_id_ruby/errors/session_not_found_error.rb +8 -0
- data/lib/smart_id_ruby/errors/session_secret_mismatch_error.rb +8 -0
- data/lib/smart_id_ruby/errors/session_timeout_error.rb +12 -0
- data/lib/smart_id_ruby/errors/smart_id_server_error.rb +12 -0
- data/lib/smart_id_ruby/errors/unprocessable_response_error.rb +9 -0
- data/lib/smart_id_ruby/errors/unsupported_client_api_version_error.rb +8 -0
- data/lib/smart_id_ruby/errors/user_account_not_found_error.rb +8 -0
- data/lib/smart_id_ruby/errors/user_account_unusable_error.rb +12 -0
- data/lib/smart_id_ruby/errors/user_refused_cert_choice_error.rb +14 -0
- data/lib/smart_id_ruby/errors/user_refused_confirmation_message_error.rb +13 -0
- data/lib/smart_id_ruby/errors/user_refused_confirmation_message_with_verification_choice_error.rb +13 -0
- data/lib/smart_id_ruby/errors/user_refused_display_text_and_pin_error.rb +13 -0
- data/lib/smart_id_ruby/errors/user_refused_error.rb +12 -0
- data/lib/smart_id_ruby/errors/user_selected_wrong_verification_code_error.rb +13 -0
- data/lib/smart_id_ruby/errors.rb +31 -0
- data/lib/smart_id_ruby/flows/base_builder.rb +90 -0
- data/lib/smart_id_ruby/flows/certificate_by_document_number_request_builder.rb +130 -0
- data/lib/smart_id_ruby/flows/device_link_authentication_session_request_builder.rb +208 -0
- data/lib/smart_id_ruby/flows/device_link_certificate_choice_session_request_builder.rb +112 -0
- data/lib/smart_id_ruby/flows/device_link_signature_session_request_builder.rb +286 -0
- data/lib/smart_id_ruby/flows/linked_notification_signature_session_request_builder.rb +235 -0
- data/lib/smart_id_ruby/flows/notification_authentication_session_request_builder.rb +184 -0
- data/lib/smart_id_ruby/flows/notification_certificate_choice_session_request_builder.rb +96 -0
- data/lib/smart_id_ruby/flows/notification_signature_session_request_builder.rb +272 -0
- data/lib/smart_id_ruby/models/authentication_identity.rb +19 -0
- data/lib/smart_id_ruby/models/authentication_response.rb +38 -0
- data/lib/smart_id_ruby/models/certificate_choice_response.rb +19 -0
- data/lib/smart_id_ruby/models/device_link_session_response.rb +34 -0
- data/lib/smart_id_ruby/models/notification_authentication_session_response.rb +25 -0
- data/lib/smart_id_ruby/models/notification_certificate_choice_session_response.rb +25 -0
- data/lib/smart_id_ruby/models/notification_signature_session_response.rb +29 -0
- data/lib/smart_id_ruby/models/session_status.rb +261 -0
- data/lib/smart_id_ruby/models/signature_response.rb +38 -0
- data/lib/smart_id_ruby/notification_interaction.rb +70 -0
- data/lib/smart_id_ruby/qr_code_generator.rb +65 -0
- data/lib/smart_id_ruby/rest/connector.rb +364 -0
- data/lib/smart_id_ruby/rest/session_status_poller.rb +125 -0
- data/lib/smart_id_ruby/rp_challenge.rb +37 -0
- data/lib/smart_id_ruby/rp_challenge_generator.rb +28 -0
- data/lib/smart_id_ruby/semantics_identifier.rb +35 -0
- data/lib/smart_id_ruby/validation/authentication_certificate_validator.rb +90 -0
- data/lib/smart_id_ruby/validation/authentication_identity_mapper.rb +227 -0
- data/lib/smart_id_ruby/validation/base_authentication_response_validator.rb +304 -0
- data/lib/smart_id_ruby/validation/certificate_choice_response_validator.rb +104 -0
- data/lib/smart_id_ruby/validation/certificate_validator.rb +170 -0
- data/lib/smart_id_ruby/validation/device_link_authentication_response_validator.rb +76 -0
- data/lib/smart_id_ruby/validation/error_result_handler.rb +88 -0
- data/lib/smart_id_ruby/validation/notification_authentication_response_validator.rb +16 -0
- data/lib/smart_id_ruby/validation/signature_payload_builder.rb +62 -0
- data/lib/smart_id_ruby/validation/signature_response_validator.rb +345 -0
- data/lib/smart_id_ruby/validation/signature_value_validator.rb +76 -0
- data/lib/smart_id_ruby/validation/trusted_ca_cert_store.rb +20 -0
- data/lib/smart_id_ruby/verification_code_calculator.rb +31 -0
- data/lib/smart_id_ruby/version.rb +5 -0
- data/lib/smart_id_ruby.rb +76 -0
- metadata +173 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module SmartIdRuby
|
|
7
|
+
module Flows
|
|
8
|
+
# Builds device link signature session requests.
|
|
9
|
+
class DeviceLinkSignatureSessionRequestBuilder < BaseBuilder
|
|
10
|
+
INITIAL_CALLBACK_URL_PATTERN = %r{\Ahttps://[^|]+\z}
|
|
11
|
+
NONCE_MAX_LENGTH = 30
|
|
12
|
+
DEFAULT_HASH_ALGORITHM = "SHA-512"
|
|
13
|
+
|
|
14
|
+
attr_reader :device_link_signature_session_request
|
|
15
|
+
|
|
16
|
+
def initialize(connector)
|
|
17
|
+
super(connector)
|
|
18
|
+
@document_number = nil
|
|
19
|
+
@semantics_identifier = nil
|
|
20
|
+
@certificate_level = nil
|
|
21
|
+
@nonce = nil
|
|
22
|
+
@capabilities = nil
|
|
23
|
+
@interactions = nil
|
|
24
|
+
@share_md_client_ip_address = nil
|
|
25
|
+
@signature_algorithm = "rsassa-pss"
|
|
26
|
+
@initial_callback_url = nil
|
|
27
|
+
@digest_input = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def with_document_number(document_number)
|
|
31
|
+
@document_number = document_number
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def with_semantics_identifier(semantics_identifier)
|
|
36
|
+
@semantics_identifier = semantics_identifier
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def with_certificate_level(certificate_level)
|
|
41
|
+
@certificate_level = certificate_level
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def with_nonce(nonce)
|
|
46
|
+
@nonce = nonce
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def with_capabilities(*capabilities)
|
|
51
|
+
@capabilities = normalize_capabilities(capabilities)
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def with_interactions(interactions)
|
|
56
|
+
@interactions = interactions
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def with_share_md_client_ip_address(share_md_client_ip_address)
|
|
61
|
+
@share_md_client_ip_address = share_md_client_ip_address
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def with_signature_algorithm(signature_algorithm)
|
|
66
|
+
@signature_algorithm = signature_algorithm
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def with_signable_data(signable_data)
|
|
71
|
+
if digest_input_kind == :signable_hash
|
|
72
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'digestInput' has already been set with SignableHash."
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
@digest_input = build_signable_data_digest_input(signable_data)
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def with_signable_hash(signable_hash)
|
|
80
|
+
if digest_input_kind == :signable_data
|
|
81
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'digestInput' has already been set with SignableData."
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
@digest_input = build_signable_hash_digest_input(signable_hash)
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def with_initial_callback_url(initial_callback_url)
|
|
89
|
+
@initial_callback_url = initial_callback_url
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def init_signature_session
|
|
94
|
+
validate_request_parameters
|
|
95
|
+
request = create_signature_session_request
|
|
96
|
+
response = init_session(request)
|
|
97
|
+
validate_response_parameters(response)
|
|
98
|
+
@device_link_signature_session_request = request
|
|
99
|
+
SmartIdRuby::Models::DeviceLinkSessionResponse.from_h(response)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def signature_session_request
|
|
103
|
+
if device_link_signature_session_request.nil?
|
|
104
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Signature session has not been initiated yet"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
device_link_signature_session_request
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def init_session(request)
|
|
113
|
+
if @semantics_identifier && @document_number
|
|
114
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Only one of 'semanticsIdentifier' or 'documentNumber' may be set"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if !blank?(@document_number)
|
|
118
|
+
connector.init_device_link_signature_with_document(request, @document_number)
|
|
119
|
+
elsif @semantics_identifier
|
|
120
|
+
connector.init_device_link_signature(request, @semantics_identifier)
|
|
121
|
+
else
|
|
122
|
+
raise SmartIdRuby::Errors::RequestSetupError,
|
|
123
|
+
"Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def create_signature_session_request
|
|
128
|
+
{
|
|
129
|
+
relyingPartyUUID: relying_party_uuid,
|
|
130
|
+
relyingPartyName: relying_party_name,
|
|
131
|
+
certificateLevel: @certificate_level&.to_s,
|
|
132
|
+
signatureProtocol: "RAW_DIGEST_SIGNATURE",
|
|
133
|
+
signatureProtocolParameters: {
|
|
134
|
+
digest: @digest_input[:digest],
|
|
135
|
+
signatureAlgorithm: @signature_algorithm.to_s,
|
|
136
|
+
signatureAlgorithmParameters: {
|
|
137
|
+
hashAlgorithm: @digest_input[:hash_algorithm].to_s
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
nonce: @nonce,
|
|
141
|
+
capabilities: @capabilities,
|
|
142
|
+
interactions: encode_interactions(@interactions),
|
|
143
|
+
requestProperties: request_properties,
|
|
144
|
+
initialCallbackUrl: @initial_callback_url
|
|
145
|
+
}.compact
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def validate_request_parameters
|
|
149
|
+
if blank?(relying_party_uuid)
|
|
150
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'relyingPartyUUID' cannot be empty"
|
|
151
|
+
end
|
|
152
|
+
if blank?(relying_party_name)
|
|
153
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'relyingPartyName' cannot be empty"
|
|
154
|
+
end
|
|
155
|
+
if @signature_algorithm.nil?
|
|
156
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'signatureAlgorithm' must be set"
|
|
157
|
+
end
|
|
158
|
+
if @digest_input.nil?
|
|
159
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'digestInput' must be set with either SignableData or SignableHash"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
validate_interactions
|
|
163
|
+
validate_initial_callback_url
|
|
164
|
+
validate_nonce
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def validate_interactions
|
|
168
|
+
normalized_interactions = normalize_interactions(@interactions)
|
|
169
|
+
if normalized_interactions.empty?
|
|
170
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'interactions' cannot be empty"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
interaction_types = normalized_interactions.map { |interaction| interaction[:type] }
|
|
174
|
+
if interaction_types.uniq.length != interaction_types.length
|
|
175
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'interactions' cannot contain duplicate types"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def validate_initial_callback_url
|
|
180
|
+
return if blank?(@initial_callback_url)
|
|
181
|
+
return if INITIAL_CALLBACK_URL_PATTERN.match?(@initial_callback_url)
|
|
182
|
+
|
|
183
|
+
raise SmartIdRuby::Errors::RequestSetupError,
|
|
184
|
+
"Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def validate_nonce
|
|
188
|
+
return if @nonce.nil?
|
|
189
|
+
return if @nonce.length.between?(1, NONCE_MAX_LENGTH)
|
|
190
|
+
|
|
191
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'nonce' length must be between 1 and 30 characters."
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def validate_response_parameters(response)
|
|
195
|
+
if blank?(fetch_value(response, :sessionID))
|
|
196
|
+
raise SmartIdRuby::Errors::UnprocessableResponseError,
|
|
197
|
+
"Device link signature session initialisation response field 'sessionID' is missing or empty"
|
|
198
|
+
end
|
|
199
|
+
if blank?(fetch_value(response, :sessionToken))
|
|
200
|
+
raise SmartIdRuby::Errors::UnprocessableResponseError,
|
|
201
|
+
"Device link signature session initialisation response field 'sessionToken' is missing or empty"
|
|
202
|
+
end
|
|
203
|
+
if blank?(fetch_value(response, :sessionSecret))
|
|
204
|
+
raise SmartIdRuby::Errors::UnprocessableResponseError,
|
|
205
|
+
"Device link signature session initialisation response field 'sessionSecret' is missing or empty"
|
|
206
|
+
end
|
|
207
|
+
if blank?(fetch_value(response, :deviceLinkBase))
|
|
208
|
+
raise SmartIdRuby::Errors::UnprocessableResponseError,
|
|
209
|
+
"Device link signature session initialisation response field 'deviceLinkBase' is missing or empty"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def request_properties
|
|
214
|
+
request_properties_for_share_md(@share_md_client_ip_address)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def build_signable_data_digest_input(signable_data)
|
|
218
|
+
return nil if signable_data.nil?
|
|
219
|
+
|
|
220
|
+
data_to_sign, hash_algorithm = extract_signable_data(signable_data)
|
|
221
|
+
data = normalize_binary_input(data_to_sign)
|
|
222
|
+
algorithm_name = normalize_hash_algorithm(hash_algorithm)
|
|
223
|
+
|
|
224
|
+
digest = OpenSSL::Digest.new(openssl_algorithm_name(algorithm_name)).digest(data)
|
|
225
|
+
{ kind: :signable_data, digest: Base64.strict_encode64(digest), hash_algorithm: algorithm_name }
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def build_signable_hash_digest_input(signable_hash)
|
|
229
|
+
return nil if signable_hash.nil?
|
|
230
|
+
|
|
231
|
+
hash_to_sign, hash_algorithm = extract_signable_hash(signable_hash)
|
|
232
|
+
hash_bytes = normalize_binary_input(hash_to_sign)
|
|
233
|
+
algorithm_name = normalize_hash_algorithm(hash_algorithm)
|
|
234
|
+
|
|
235
|
+
{ kind: :signable_hash, digest: Base64.strict_encode64(hash_bytes), hash_algorithm: algorithm_name }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def extract_signable_data(input)
|
|
239
|
+
if input.respond_to?(:to_h)
|
|
240
|
+
normalized = input.to_h.transform_keys(&:to_sym)
|
|
241
|
+
[normalized[:data_to_sign] || normalized[:data], normalized[:hash_algorithm]]
|
|
242
|
+
elsif input.respond_to?(:data_to_sign)
|
|
243
|
+
[input.data_to_sign, input.respond_to?(:hash_algorithm) ? input.hash_algorithm : nil]
|
|
244
|
+
else
|
|
245
|
+
[input, nil]
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def extract_signable_hash(input)
|
|
250
|
+
if input.respond_to?(:to_h)
|
|
251
|
+
normalized = input.to_h.transform_keys(&:to_sym)
|
|
252
|
+
[normalized[:hash_to_sign] || normalized[:hash] || normalized[:digest], normalized[:hash_algorithm]]
|
|
253
|
+
elsif input.respond_to?(:hash_to_sign)
|
|
254
|
+
[input.hash_to_sign, input.respond_to?(:hash_algorithm) ? input.hash_algorithm : nil]
|
|
255
|
+
else
|
|
256
|
+
[input, nil]
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def normalize_binary_input(input)
|
|
261
|
+
data = input.is_a?(String) ? input.dup : input
|
|
262
|
+
data = data.pack("C*") if data.is_a?(Array) && data.all? { |value| value.is_a?(Integer) && value.between?(0, 255) }
|
|
263
|
+
if data.nil? || data.to_s.bytesize.zero?
|
|
264
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'digestInput' must be set with either SignableData or SignableHash"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
data
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def normalize_hash_algorithm(hash_algorithm)
|
|
271
|
+
value = hash_algorithm&.to_s
|
|
272
|
+
return DEFAULT_HASH_ALGORITHM if blank?(value)
|
|
273
|
+
|
|
274
|
+
value
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def openssl_algorithm_name(hash_algorithm)
|
|
278
|
+
hash_algorithm.to_s.delete("-")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def digest_input_kind
|
|
282
|
+
@digest_input && @digest_input[:kind]
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module SmartIdRuby
|
|
7
|
+
module Flows
|
|
8
|
+
# Builds linked notification signature session requests.
|
|
9
|
+
class LinkedNotificationSignatureSessionRequestBuilder < BaseBuilder
|
|
10
|
+
NONCE_MAX_LENGTH = 30
|
|
11
|
+
DEFAULT_HASH_ALGORITHM = "SHA-512"
|
|
12
|
+
|
|
13
|
+
def initialize(connector)
|
|
14
|
+
super(connector)
|
|
15
|
+
@document_number = nil
|
|
16
|
+
@digest_input = nil
|
|
17
|
+
@signature_algorithm = "rsassa-pss"
|
|
18
|
+
@linked_session_id = nil
|
|
19
|
+
@interactions = nil
|
|
20
|
+
@certificate_level = nil
|
|
21
|
+
@nonce = nil
|
|
22
|
+
@share_md_client_ip_address = nil
|
|
23
|
+
@capabilities = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def with_document_number(document_number)
|
|
27
|
+
@document_number = document_number
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def with_certificate_level(certificate_level)
|
|
32
|
+
@certificate_level = certificate_level
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def with_signable_data(signable_data)
|
|
37
|
+
if digest_input_kind == :signable_hash
|
|
38
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'digestInput' has been already set with SignableHash"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@digest_input = build_signable_data_digest_input(signable_data)
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def with_signable_hash(signable_hash)
|
|
46
|
+
if digest_input_kind == :signable_data
|
|
47
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'digestInput' has been already set with SignableData"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@digest_input = build_signable_hash_digest_input(signable_hash)
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def with_signature_algorithm(signature_algorithm)
|
|
55
|
+
@signature_algorithm = signature_algorithm
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def with_linked_session_id(linked_session_id)
|
|
60
|
+
@linked_session_id = linked_session_id
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def with_nonce(nonce)
|
|
65
|
+
@nonce = nonce
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def with_interactions(interactions)
|
|
70
|
+
@interactions = interactions
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def with_share_md_client_ip_address(share_md_client_ip_address)
|
|
75
|
+
@share_md_client_ip_address = share_md_client_ip_address
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def with_capabilities(*capabilities)
|
|
80
|
+
@capabilities = normalize_capabilities(capabilities)
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def init_signature_session
|
|
85
|
+
validate_request_parameters
|
|
86
|
+
request = create_session_request
|
|
87
|
+
response = connector.init_linked_notification_signature(request, @document_number)
|
|
88
|
+
validate_response(response)
|
|
89
|
+
response
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def validate_request_parameters
|
|
95
|
+
if blank?(relying_party_uuid)
|
|
96
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'relyingPartyUUID' cannot be empty"
|
|
97
|
+
end
|
|
98
|
+
if blank?(relying_party_name)
|
|
99
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'relyingPartyName' cannot be empty"
|
|
100
|
+
end
|
|
101
|
+
if blank?(@document_number)
|
|
102
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'documentNumber' cannot be empty"
|
|
103
|
+
end
|
|
104
|
+
if @digest_input.nil?
|
|
105
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'digestInput' must be set with SignableData or with SignableHash"
|
|
106
|
+
end
|
|
107
|
+
if @signature_algorithm.nil?
|
|
108
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'signatureAlgorithm' must be set"
|
|
109
|
+
end
|
|
110
|
+
if blank?(@linked_session_id)
|
|
111
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'linkedSessionID' cannot be empty"
|
|
112
|
+
end
|
|
113
|
+
if !@nonce.nil? && (@nonce.empty? || @nonce.length > NONCE_MAX_LENGTH)
|
|
114
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'nonce' must be 1-30 characters long"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
validate_interactions
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def validate_interactions
|
|
121
|
+
normalized_interactions = normalize_interactions(@interactions)
|
|
122
|
+
if normalized_interactions.empty?
|
|
123
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'interactions' cannot be empty"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
interaction_types = normalized_interactions.map { |interaction| interaction[:type] }
|
|
127
|
+
if interaction_types.uniq.length != interaction_types.length
|
|
128
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'interactions' cannot contain duplicate types"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def create_session_request
|
|
133
|
+
{
|
|
134
|
+
relyingPartyUUID: relying_party_uuid,
|
|
135
|
+
relyingPartyName: relying_party_name,
|
|
136
|
+
certificateLevel: @certificate_level&.to_s,
|
|
137
|
+
signatureProtocol: "RAW_DIGEST_SIGNATURE",
|
|
138
|
+
signatureProtocolParameters: {
|
|
139
|
+
digest: @digest_input[:digest],
|
|
140
|
+
signatureAlgorithm: @signature_algorithm.to_s,
|
|
141
|
+
signatureAlgorithmParameters: {
|
|
142
|
+
hashAlgorithm: @digest_input[:hash_algorithm].to_s
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
linkedSessionID: @linked_session_id,
|
|
146
|
+
nonce: @nonce,
|
|
147
|
+
interactions: encode_interactions(@interactions),
|
|
148
|
+
requestProperties: request_properties,
|
|
149
|
+
capabilities: @capabilities
|
|
150
|
+
}.compact
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def request_properties
|
|
154
|
+
request_properties_for_share_md(@share_md_client_ip_address)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def validate_response(response)
|
|
158
|
+
return unless blank?(fetch_value(response, :sessionID))
|
|
159
|
+
|
|
160
|
+
raise SmartIdRuby::Errors::UnprocessableResponseError,
|
|
161
|
+
"Linked notification-base signature session response field 'sessionID' is missing or empty"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def build_signable_data_digest_input(signable_data)
|
|
165
|
+
return nil if signable_data.nil?
|
|
166
|
+
|
|
167
|
+
data_to_sign, hash_algorithm = extract_signable_data(signable_data)
|
|
168
|
+
data = normalize_binary_input(data_to_sign)
|
|
169
|
+
algorithm_name = normalize_hash_algorithm(hash_algorithm)
|
|
170
|
+
digest = OpenSSL::Digest.new(openssl_algorithm_name(algorithm_name)).digest(data)
|
|
171
|
+
{ kind: :signable_data, digest: Base64.strict_encode64(digest), hash_algorithm: algorithm_name }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def build_signable_hash_digest_input(signable_hash)
|
|
175
|
+
return nil if signable_hash.nil?
|
|
176
|
+
|
|
177
|
+
hash_to_sign, hash_algorithm = extract_signable_hash(signable_hash)
|
|
178
|
+
hash_bytes = normalize_binary_input(hash_to_sign)
|
|
179
|
+
algorithm_name = normalize_hash_algorithm(hash_algorithm)
|
|
180
|
+
{ kind: :signable_hash, digest: Base64.strict_encode64(hash_bytes), hash_algorithm: algorithm_name }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def extract_signable_data(input)
|
|
184
|
+
if input.respond_to?(:to_h)
|
|
185
|
+
normalized = input.to_h.transform_keys(&:to_sym)
|
|
186
|
+
[normalized[:data_to_sign] || normalized[:data], normalized[:hash_algorithm]]
|
|
187
|
+
elsif input.respond_to?(:data_to_sign)
|
|
188
|
+
[input.data_to_sign, input.respond_to?(:hash_algorithm) ? input.hash_algorithm : nil]
|
|
189
|
+
else
|
|
190
|
+
[input, nil]
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def extract_signable_hash(input)
|
|
195
|
+
if input.respond_to?(:to_h)
|
|
196
|
+
normalized = input.to_h.transform_keys(&:to_sym)
|
|
197
|
+
[normalized[:hash_to_sign] || normalized[:hash] || normalized[:digest], normalized[:hash_algorithm]]
|
|
198
|
+
elsif input.respond_to?(:hash_to_sign)
|
|
199
|
+
[input.hash_to_sign, input.respond_to?(:hash_algorithm) ? input.hash_algorithm : nil]
|
|
200
|
+
else
|
|
201
|
+
[input, nil]
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def normalize_binary_input(input)
|
|
206
|
+
data = input.is_a?(String) ? input.dup : input
|
|
207
|
+
if data.is_a?(Array) && data.all? { |value| value.is_a?(Integer) && value.between?(0, 255) }
|
|
208
|
+
data = data.pack("C*")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
if data.nil? || data.to_s.bytesize.zero?
|
|
212
|
+
raise SmartIdRuby::Errors::RequestSetupError,
|
|
213
|
+
"Value for 'digestInput' must be set with SignableData or with SignableHash"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
data
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def normalize_hash_algorithm(hash_algorithm)
|
|
220
|
+
value = hash_algorithm&.to_s
|
|
221
|
+
return DEFAULT_HASH_ALGORITHM if blank?(value)
|
|
222
|
+
|
|
223
|
+
value
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def openssl_algorithm_name(hash_algorithm)
|
|
227
|
+
hash_algorithm.to_s.delete("-")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def digest_input_kind
|
|
231
|
+
@digest_input && @digest_input[:kind]
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module SmartIdRuby
|
|
6
|
+
module Flows
|
|
7
|
+
# Builds notification authentication session requests.
|
|
8
|
+
class NotificationAuthenticationSessionRequestBuilder < BaseBuilder
|
|
9
|
+
RP_CHALLENGE_MIN_LENGTH = 44
|
|
10
|
+
RP_CHALLENGE_MAX_LENGTH = 88
|
|
11
|
+
|
|
12
|
+
attr_reader :notification_authentication_session_request
|
|
13
|
+
|
|
14
|
+
def initialize(connector)
|
|
15
|
+
super(connector)
|
|
16
|
+
@certificate_level = nil
|
|
17
|
+
@signature_algorithm = "rsassa-pss"
|
|
18
|
+
@hash_algorithm = "SHA3-512"
|
|
19
|
+
@interactions = nil
|
|
20
|
+
@share_md_client_ip_address = nil
|
|
21
|
+
@capabilities = nil
|
|
22
|
+
@semantics_identifier = nil
|
|
23
|
+
@document_number = nil
|
|
24
|
+
@rp_challenge = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def with_certificate_level(certificate_level)
|
|
28
|
+
@certificate_level = certificate_level
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def with_rp_challenge(rp_challenge)
|
|
33
|
+
@rp_challenge = rp_challenge
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def with_signature_algorithm(signature_algorithm)
|
|
38
|
+
@signature_algorithm = signature_algorithm
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def with_hash_algorithm(hash_algorithm)
|
|
43
|
+
@hash_algorithm = hash_algorithm
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def with_interactions(interactions)
|
|
48
|
+
@interactions = interactions
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def with_share_md_client_ip_address(share_md_client_ip_address)
|
|
53
|
+
@share_md_client_ip_address = share_md_client_ip_address
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def with_capabilities(*capabilities)
|
|
58
|
+
@capabilities = normalize_capabilities(capabilities)
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def with_semantics_identifier(semantics_identifier)
|
|
63
|
+
@semantics_identifier = semantics_identifier
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def with_document_number(document_number)
|
|
68
|
+
@document_number = document_number
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def init_authentication_session
|
|
73
|
+
validate_request_parameters
|
|
74
|
+
request = create_authentication_request
|
|
75
|
+
response = init_session(request)
|
|
76
|
+
validate_response_parameters(response)
|
|
77
|
+
@notification_authentication_session_request = request
|
|
78
|
+
SmartIdRuby::Models::NotificationAuthenticationSessionResponse.from_h(response)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def authentication_session_request
|
|
82
|
+
if notification_authentication_session_request.nil?
|
|
83
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Notification-based authentication session has not been initialized yet"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
notification_authentication_session_request
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def init_session(request)
|
|
92
|
+
if @semantics_identifier && @document_number
|
|
93
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Only one of 'semanticsIdentifier' or 'documentNumber' may be set"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if @semantics_identifier
|
|
97
|
+
connector.init_notification_authentication(request, @semantics_identifier)
|
|
98
|
+
elsif @document_number
|
|
99
|
+
connector.init_notification_authentication_with_document(request, @document_number)
|
|
100
|
+
else
|
|
101
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Either 'documentNumber' or 'semanticsIdentifier' must be set"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def validate_request_parameters
|
|
106
|
+
if blank?(relying_party_uuid)
|
|
107
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'relyingPartyUUID' cannot be empty"
|
|
108
|
+
end
|
|
109
|
+
if blank?(relying_party_name)
|
|
110
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'relyingPartyName' cannot be empty"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
validate_signature_parameters
|
|
114
|
+
validate_interactions
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def validate_signature_parameters
|
|
118
|
+
if blank?(@rp_challenge)
|
|
119
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'rpChallenge' cannot be empty"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
begin
|
|
123
|
+
Base64.strict_decode64(@rp_challenge)
|
|
124
|
+
rescue ArgumentError
|
|
125
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'rpChallenge' must be Base64-encoded string"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
unless @rp_challenge.length.between?(RP_CHALLENGE_MIN_LENGTH, RP_CHALLENGE_MAX_LENGTH)
|
|
129
|
+
raise SmartIdRuby::Errors::RequestSetupError,
|
|
130
|
+
"Value for 'rpChallenge' must have length between #{RP_CHALLENGE_MIN_LENGTH} and #{RP_CHALLENGE_MAX_LENGTH} characters"
|
|
131
|
+
end
|
|
132
|
+
if @signature_algorithm.nil?
|
|
133
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'signatureAlgorithm' must be set"
|
|
134
|
+
end
|
|
135
|
+
if @hash_algorithm.nil?
|
|
136
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'hashAlgorithm' must be set"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def validate_interactions
|
|
141
|
+
normalized_interactions = normalize_interactions(@interactions)
|
|
142
|
+
if normalized_interactions.empty?
|
|
143
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'interactions' cannot be empty"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
interaction_types = normalized_interactions.map { |interaction| interaction[:type] }
|
|
147
|
+
if interaction_types.uniq.length != interaction_types.length
|
|
148
|
+
raise SmartIdRuby::Errors::RequestSetupError, "Value for 'interactions' cannot contain duplicate types"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def create_authentication_request
|
|
153
|
+
{
|
|
154
|
+
relyingPartyUUID: relying_party_uuid,
|
|
155
|
+
relyingPartyName: relying_party_name,
|
|
156
|
+
certificateLevel: @certificate_level&.to_s,
|
|
157
|
+
signatureProtocol: "ACSP_V2",
|
|
158
|
+
signatureProtocolParameters: {
|
|
159
|
+
rpChallenge: @rp_challenge,
|
|
160
|
+
signatureAlgorithm: @signature_algorithm.to_s,
|
|
161
|
+
signatureAlgorithmParameters: {
|
|
162
|
+
hashAlgorithm: @hash_algorithm.to_s
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
interactions: encode_interactions(@interactions),
|
|
166
|
+
requestProperties: request_properties,
|
|
167
|
+
capabilities: @capabilities,
|
|
168
|
+
vcType: "numeric4"
|
|
169
|
+
}.compact
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def request_properties
|
|
173
|
+
request_properties_for_share_md(@share_md_client_ip_address)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def validate_response_parameters(response)
|
|
177
|
+
return unless blank?(fetch_value(response, :sessionID))
|
|
178
|
+
|
|
179
|
+
raise SmartIdRuby::Errors::UnprocessableResponseError,
|
|
180
|
+
"Notification-based authentication session initialisation response field 'sessionID' is missing or empty"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|