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.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +14 -0
  4. data/CHANGELOG.md +13 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +436 -0
  8. data/Rakefile +12 -0
  9. data/lib/smart-id-ruby-client.rb +3 -0
  10. data/lib/smart_id_ruby/callback_url.rb +18 -0
  11. data/lib/smart_id_ruby/callback_url_util.rb +54 -0
  12. data/lib/smart_id_ruby/client.rb +124 -0
  13. data/lib/smart_id_ruby/configuration.rb +184 -0
  14. data/lib/smart_id_ruby/device_link_builder.rb +301 -0
  15. data/lib/smart_id_ruby/device_link_interaction.rb +67 -0
  16. data/lib/smart_id_ruby/errors/certificate_level_mismatch_error.rb +8 -0
  17. data/lib/smart_id_ruby/errors/document_unusable_error.rb +12 -0
  18. data/lib/smart_id_ruby/errors/error.rb +8 -0
  19. data/lib/smart_id_ruby/errors/expected_linked_session_error.rb +15 -0
  20. data/lib/smart_id_ruby/errors/no_suitable_account_of_requested_type_found_error.rb +10 -0
  21. data/lib/smart_id_ruby/errors/person_should_view_smart_id_portal_error.rb +8 -0
  22. data/lib/smart_id_ruby/errors/protocol_failure_error.rb +13 -0
  23. data/lib/smart_id_ruby/errors/relying_party_account_configuration_error.rb +10 -0
  24. data/lib/smart_id_ruby/errors/request_setup_error.rb +10 -0
  25. data/lib/smart_id_ruby/errors/request_validation_error.rb +8 -0
  26. data/lib/smart_id_ruby/errors/required_interaction_not_supported_by_app_error.rb +13 -0
  27. data/lib/smart_id_ruby/errors/response_error.rb +8 -0
  28. data/lib/smart_id_ruby/errors/server_maintenance_error.rb +8 -0
  29. data/lib/smart_id_ruby/errors/session_end_result_error.rb +15 -0
  30. data/lib/smart_id_ruby/errors/session_not_complete_error.rb +8 -0
  31. data/lib/smart_id_ruby/errors/session_not_found_error.rb +8 -0
  32. data/lib/smart_id_ruby/errors/session_secret_mismatch_error.rb +8 -0
  33. data/lib/smart_id_ruby/errors/session_timeout_error.rb +12 -0
  34. data/lib/smart_id_ruby/errors/smart_id_server_error.rb +12 -0
  35. data/lib/smart_id_ruby/errors/unprocessable_response_error.rb +9 -0
  36. data/lib/smart_id_ruby/errors/unsupported_client_api_version_error.rb +8 -0
  37. data/lib/smart_id_ruby/errors/user_account_not_found_error.rb +8 -0
  38. data/lib/smart_id_ruby/errors/user_account_unusable_error.rb +12 -0
  39. data/lib/smart_id_ruby/errors/user_refused_cert_choice_error.rb +14 -0
  40. data/lib/smart_id_ruby/errors/user_refused_confirmation_message_error.rb +13 -0
  41. data/lib/smart_id_ruby/errors/user_refused_confirmation_message_with_verification_choice_error.rb +13 -0
  42. data/lib/smart_id_ruby/errors/user_refused_display_text_and_pin_error.rb +13 -0
  43. data/lib/smart_id_ruby/errors/user_refused_error.rb +12 -0
  44. data/lib/smart_id_ruby/errors/user_selected_wrong_verification_code_error.rb +13 -0
  45. data/lib/smart_id_ruby/errors.rb +31 -0
  46. data/lib/smart_id_ruby/flows/base_builder.rb +90 -0
  47. data/lib/smart_id_ruby/flows/certificate_by_document_number_request_builder.rb +130 -0
  48. data/lib/smart_id_ruby/flows/device_link_authentication_session_request_builder.rb +208 -0
  49. data/lib/smart_id_ruby/flows/device_link_certificate_choice_session_request_builder.rb +112 -0
  50. data/lib/smart_id_ruby/flows/device_link_signature_session_request_builder.rb +286 -0
  51. data/lib/smart_id_ruby/flows/linked_notification_signature_session_request_builder.rb +235 -0
  52. data/lib/smart_id_ruby/flows/notification_authentication_session_request_builder.rb +184 -0
  53. data/lib/smart_id_ruby/flows/notification_certificate_choice_session_request_builder.rb +96 -0
  54. data/lib/smart_id_ruby/flows/notification_signature_session_request_builder.rb +272 -0
  55. data/lib/smart_id_ruby/models/authentication_identity.rb +19 -0
  56. data/lib/smart_id_ruby/models/authentication_response.rb +38 -0
  57. data/lib/smart_id_ruby/models/certificate_choice_response.rb +19 -0
  58. data/lib/smart_id_ruby/models/device_link_session_response.rb +34 -0
  59. data/lib/smart_id_ruby/models/notification_authentication_session_response.rb +25 -0
  60. data/lib/smart_id_ruby/models/notification_certificate_choice_session_response.rb +25 -0
  61. data/lib/smart_id_ruby/models/notification_signature_session_response.rb +29 -0
  62. data/lib/smart_id_ruby/models/session_status.rb +261 -0
  63. data/lib/smart_id_ruby/models/signature_response.rb +38 -0
  64. data/lib/smart_id_ruby/notification_interaction.rb +70 -0
  65. data/lib/smart_id_ruby/qr_code_generator.rb +65 -0
  66. data/lib/smart_id_ruby/rest/connector.rb +364 -0
  67. data/lib/smart_id_ruby/rest/session_status_poller.rb +125 -0
  68. data/lib/smart_id_ruby/rp_challenge.rb +37 -0
  69. data/lib/smart_id_ruby/rp_challenge_generator.rb +28 -0
  70. data/lib/smart_id_ruby/semantics_identifier.rb +35 -0
  71. data/lib/smart_id_ruby/validation/authentication_certificate_validator.rb +90 -0
  72. data/lib/smart_id_ruby/validation/authentication_identity_mapper.rb +227 -0
  73. data/lib/smart_id_ruby/validation/base_authentication_response_validator.rb +304 -0
  74. data/lib/smart_id_ruby/validation/certificate_choice_response_validator.rb +104 -0
  75. data/lib/smart_id_ruby/validation/certificate_validator.rb +170 -0
  76. data/lib/smart_id_ruby/validation/device_link_authentication_response_validator.rb +76 -0
  77. data/lib/smart_id_ruby/validation/error_result_handler.rb +88 -0
  78. data/lib/smart_id_ruby/validation/notification_authentication_response_validator.rb +16 -0
  79. data/lib/smart_id_ruby/validation/signature_payload_builder.rb +62 -0
  80. data/lib/smart_id_ruby/validation/signature_response_validator.rb +345 -0
  81. data/lib/smart_id_ruby/validation/signature_value_validator.rb +76 -0
  82. data/lib/smart_id_ruby/validation/trusted_ca_cert_store.rb +20 -0
  83. data/lib/smart_id_ruby/verification_code_calculator.rb +31 -0
  84. data/lib/smart_id_ruby/version.rb +5 -0
  85. data/lib/smart_id_ruby.rb +76 -0
  86. 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