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,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartIdRuby
4
+ module Models
5
+ # Represents response for active Smart-ID session query.
6
+ # state - Required. Current state of the session, e.g. "RUNNING", "COMPLETE".
7
+ # result - Required if state is "COMPLETE". Details about how session ended.
8
+ # signature_protocol - Required if end result is OK. Signature protocol used, e.g. "ACSP_V2" or "RAW_DIGEST_SIGNATURE".
9
+ # signature - Required if end result is OK. Signature data containing the actual signature and related information.
10
+ # cert - Required if end result is OK. Signer's certificate data.
11
+ # ignored_properties - Properties that were ignored from the session request.
12
+ # interaction_type_used - Required if end result is OK. Interaction type that was used in the session.
13
+ # device_ip_address - IP address of the device used in the session.
14
+ class SessionStatus
15
+ attr_reader :state, :result, :signature_protocol, :signature, :cert,
16
+ :ignored_properties, :interaction_type_used, :device_ip_address
17
+
18
+ def initialize(
19
+ state: nil,
20
+ result: nil,
21
+ signature_protocol: nil,
22
+ signature: nil,
23
+ cert: nil,
24
+ ignored_properties: nil,
25
+ interaction_type_used: nil,
26
+ device_ip_address: nil
27
+ )
28
+ @state = state
29
+ @result = result
30
+ @signature_protocol = signature_protocol
31
+ @signature = signature
32
+ @cert = cert
33
+ @ignored_properties = ignored_properties
34
+ @interaction_type_used = interaction_type_used
35
+ @device_ip_address = device_ip_address
36
+ end
37
+
38
+ def running?
39
+ state.to_s.casecmp("RUNNING").zero?
40
+ end
41
+
42
+ def complete?
43
+ state.to_s.casecmp("COMPLETE").zero?
44
+ end
45
+
46
+ def to_h
47
+ {
48
+ state: state,
49
+ result: result.to_h,
50
+ signatureProtocol: signature_protocol,
51
+ signature: signature.to_h,
52
+ cert: cert.to_h,
53
+ ignoredProperties: ignored_properties,
54
+ interactionTypeUsed: interaction_type_used,
55
+ deviceIpAddress: device_ip_address
56
+ }
57
+ end
58
+
59
+ def self.from_h(payload)
60
+ return new unless payload.is_a?(Hash)
61
+
62
+ new(
63
+ state: fetch(payload, :state),
64
+ result: SessionResult.from_h(fetch(payload, :result)),
65
+ signature_protocol: fetch(payload, :signatureProtocol),
66
+ signature: SessionSignature.from_h(fetch(payload, :signature)),
67
+ cert: SessionCertificate.from_h(fetch(payload, :cert)),
68
+ ignored_properties: fetch(payload, :ignoredProperties),
69
+ interaction_type_used: fetch(payload, :interactionTypeUsed),
70
+ device_ip_address: fetch(payload, :deviceIpAddress)
71
+ )
72
+ end
73
+
74
+ def self.fetch(payload, key)
75
+ payload[key] || payload[key.to_s]
76
+ end
77
+ private_class_method :fetch
78
+ end
79
+
80
+ # Represents session result data returned by Smart-ID.
81
+ class SessionResult
82
+ attr_reader :end_result, :document_number, :details
83
+
84
+ def initialize(end_result: nil, document_number: nil, details: nil)
85
+ @end_result = end_result
86
+ @document_number = document_number
87
+ @details = details
88
+ end
89
+
90
+ def to_h
91
+ {
92
+ endResult: end_result,
93
+ documentNumber: document_number,
94
+ details: details.to_h
95
+ }
96
+ end
97
+
98
+ def self.from_h(payload)
99
+ return nil unless payload.is_a?(Hash)
100
+
101
+ new(
102
+ end_result: fetch(payload, :endResult),
103
+ document_number: fetch(payload, :documentNumber),
104
+ details: SessionResultDetails.from_h(fetch(payload, :details))
105
+ )
106
+ end
107
+
108
+ def self.fetch(payload, key)
109
+ payload[key] || payload[key.to_s]
110
+ end
111
+ private_class_method :fetch
112
+ end
113
+
114
+ # Represents additional result details for a session.
115
+ class SessionResultDetails
116
+ attr_reader :interaction
117
+
118
+ def initialize(interaction: nil)
119
+ @interaction = interaction
120
+ end
121
+
122
+ def to_h
123
+ {
124
+ interaction: interaction
125
+ }
126
+ end
127
+
128
+ def self.from_h(payload)
129
+ return nil unless payload.is_a?(Hash)
130
+
131
+ new(interaction: fetch(payload, :interaction))
132
+ end
133
+
134
+ def self.fetch(payload, key)
135
+ payload[key] || payload[key.to_s]
136
+ end
137
+ private_class_method :fetch
138
+ end
139
+
140
+ # Represents signature details in session status response.
141
+ class SessionSignature
142
+ attr_reader :value, :server_random, :user_challenge, :flow_type,
143
+ :signature_algorithm, :signature_algorithm_parameters
144
+
145
+ def initialize(
146
+ value: nil,
147
+ server_random: nil,
148
+ user_challenge: nil,
149
+ flow_type: nil,
150
+ signature_algorithm: nil,
151
+ signature_algorithm_parameters: nil
152
+ )
153
+ @value = value
154
+ @server_random = server_random
155
+ @user_challenge = user_challenge
156
+ @flow_type = flow_type
157
+ @signature_algorithm = signature_algorithm
158
+ @signature_algorithm_parameters = signature_algorithm_parameters
159
+ end
160
+
161
+ def to_h
162
+ {
163
+ value: value,
164
+ serverRandom: server_random,
165
+ userChallenge: user_challenge,
166
+ flowType: flow_type,
167
+ signatureAlgorithm: signature_algorithm,
168
+ signatureAlgorithmParameters: signature_algorithm_parameters.to_h
169
+ }
170
+ end
171
+
172
+ def self.from_h(payload)
173
+ return nil unless payload.is_a?(Hash)
174
+
175
+ new(
176
+ value: fetch(payload, :value),
177
+ server_random: fetch(payload, :serverRandom),
178
+ user_challenge: fetch(payload, :userChallenge),
179
+ flow_type: fetch(payload, :flowType),
180
+ signature_algorithm: fetch(payload, :signatureAlgorithm),
181
+ signature_algorithm_parameters: SessionSignatureAlgorithmParameters.from_h(
182
+ fetch(payload, :signatureAlgorithmParameters)
183
+ )
184
+ )
185
+ end
186
+
187
+ def self.fetch(payload, key)
188
+ payload[key] || payload[key.to_s]
189
+ end
190
+ private_class_method :fetch
191
+ end
192
+
193
+ # Represents signature algorithm parameters for session signature.
194
+ class SessionSignatureAlgorithmParameters
195
+ attr_reader :hash_algorithm, :mask_gen_algorithm, :salt_length, :trailer_field
196
+
197
+ def initialize(hash_algorithm: nil, mask_gen_algorithm: nil, salt_length: nil, trailer_field: nil)
198
+ @hash_algorithm = hash_algorithm
199
+ @mask_gen_algorithm = mask_gen_algorithm
200
+ @salt_length = salt_length
201
+ @trailer_field = trailer_field
202
+ end
203
+
204
+ def to_h
205
+ {
206
+ hashAlgorithm: hash_algorithm,
207
+ maskGenAlgorithm: mask_gen_algorithm,
208
+ saltLength: salt_length,
209
+ trailerField: trailer_field
210
+ }
211
+ end
212
+
213
+ def self.from_h(payload)
214
+ return nil unless payload.is_a?(Hash)
215
+
216
+ new(
217
+ hash_algorithm: fetch(payload, :hashAlgorithm),
218
+ mask_gen_algorithm: fetch(payload, :maskGenAlgorithm),
219
+ salt_length: fetch(payload, :saltLength),
220
+ trailer_field: fetch(payload, :trailerField)
221
+ )
222
+ end
223
+
224
+ def self.fetch(payload, key)
225
+ payload[key] || payload[key.to_s]
226
+ end
227
+ private_class_method :fetch
228
+ end
229
+
230
+ # Represents certificate payload in session status response.
231
+ class SessionCertificate
232
+ attr_reader :value, :certificate_level
233
+
234
+ def initialize(value: nil, certificate_level: nil)
235
+ @value = value
236
+ @certificate_level = certificate_level
237
+ end
238
+
239
+ def to_h
240
+ {
241
+ value: value,
242
+ certificateLevel: certificate_level
243
+ }
244
+ end
245
+
246
+ def self.from_h(payload)
247
+ return nil unless payload.is_a?(Hash)
248
+
249
+ new(
250
+ value: fetch(payload, :value),
251
+ certificate_level: fetch(payload, :certificateLevel)
252
+ )
253
+ end
254
+
255
+ def self.fetch(payload, key)
256
+ payload[key] || payload[key.to_s]
257
+ end
258
+ private_class_method :fetch
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartIdRuby
4
+ module Models
5
+ # Represents validated signature response data.
6
+ class SignatureResponse
7
+ attr_reader :end_result, :signature_value_in_base64, :algorithm_name, :flow_type, :certificate,
8
+ :requested_certificate_level, :certificate_level, :document_number, :interaction_flow_used,
9
+ :device_ip_address, :rsa_ssa_pss_parameters
10
+
11
+ def initialize(
12
+ end_result:,
13
+ signature_value_in_base64:,
14
+ algorithm_name:,
15
+ flow_type:,
16
+ certificate:,
17
+ requested_certificate_level:,
18
+ certificate_level:,
19
+ document_number:,
20
+ interaction_flow_used:,
21
+ device_ip_address:,
22
+ rsa_ssa_pss_parameters:
23
+ )
24
+ @end_result = end_result
25
+ @signature_value_in_base64 = signature_value_in_base64
26
+ @algorithm_name = algorithm_name
27
+ @flow_type = flow_type
28
+ @certificate = certificate
29
+ @requested_certificate_level = requested_certificate_level
30
+ @certificate_level = certificate_level
31
+ @document_number = document_number
32
+ @interaction_flow_used = interaction_flow_used
33
+ @device_ip_address = device_ip_address
34
+ @rsa_ssa_pss_parameters = rsa_ssa_pss_parameters
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartIdRuby
4
+ # Represents interaction payload used in notification-based flows.
5
+ class NotificationInteraction
6
+ DISPLAY_TEXT_AND_PIN = "displayTextAndPIN"
7
+ CONFIRMATION_MESSAGE = "confirmationMessage"
8
+ CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE = "confirmationMessageAndVerificationCodeChoice"
9
+
10
+ DISPLAY_TEXT_60_MAX_LENGTH = 60
11
+ DISPLAY_TEXT_200_MAX_LENGTH = 200
12
+
13
+ attr_reader :type, :display_text60, :display_text200
14
+
15
+ def initialize(type:, display_text60: nil, display_text200: nil)
16
+ @type = type&.to_s
17
+ @display_text60 = display_text60
18
+ @display_text200 = display_text200
19
+
20
+ validate!
21
+ end
22
+
23
+ def to_h
24
+ {
25
+ type: type,
26
+ displayText60: display_text60,
27
+ displayText200: display_text200
28
+ }.compact
29
+ end
30
+
31
+ def self.display_text_and_pin(display_text60)
32
+ new(type: DISPLAY_TEXT_AND_PIN, display_text60: display_text60)
33
+ end
34
+
35
+ def self.confirmation_message(display_text200)
36
+ new(type: CONFIRMATION_MESSAGE, display_text200: display_text200)
37
+ end
38
+
39
+ def self.confirmation_message_and_verification_code_choice(display_text200)
40
+ new(type: CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, display_text200: display_text200)
41
+ end
42
+
43
+ private
44
+
45
+ def validate!
46
+ raise SmartIdRuby::Errors::RequestSetupError, "Value for 'type' must be set" if blank?(type)
47
+
48
+ case type
49
+ when DISPLAY_TEXT_AND_PIN
50
+ validate_display_text!(display_text60, "displayText60", DISPLAY_TEXT_60_MAX_LENGTH)
51
+ when CONFIRMATION_MESSAGE, CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE
52
+ validate_display_text!(display_text200, "displayText200", DISPLAY_TEXT_200_MAX_LENGTH)
53
+ else
54
+ raise SmartIdRuby::Errors::RequestSetupError, "Unsupported interaction type: #{type}"
55
+ end
56
+ end
57
+
58
+ def validate_display_text!(value, field_name, max_length)
59
+ raise SmartIdRuby::Errors::RequestSetupError, "Value for '#{field_name}' cannot be empty" if blank?(value)
60
+ return if value.to_s.length <= max_length
61
+
62
+ raise SmartIdRuby::Errors::RequestSetupError,
63
+ "Value for '#{field_name}' cannot be longer than #{max_length} characters"
64
+ end
65
+
66
+ def blank?(value)
67
+ value.nil? || value.to_s.strip.empty?
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "rqrcode"
5
+
6
+ module SmartIdRuby
7
+ # Utility for generating QR-code images and Data URIs.
8
+ class QrCodeGenerator
9
+ DEFAULT_QR_CODE_WIDTH_PX = 610
10
+ DEFAULT_QR_CODE_HEIGHT_PX = 610
11
+ DEFAULT_QUIET_AREA_SIZE_MODULES = 4
12
+ DEFAULT_FILE_FORMAT = "png"
13
+
14
+ class << self
15
+ def generate_data_uri(data)
16
+ image = generate_image(data)
17
+ convert_to_data_uri(image, DEFAULT_FILE_FORMAT)
18
+ end
19
+
20
+ def generate_image(
21
+ data,
22
+ width_px = DEFAULT_QR_CODE_WIDTH_PX,
23
+ height_px = DEFAULT_QR_CODE_HEIGHT_PX,
24
+ quiet_area_size = DEFAULT_QUIET_AREA_SIZE_MODULES
25
+ )
26
+ validate_data!(data)
27
+
28
+ qrcode = RQRCode::QRCode.new(data.to_s, level: :l)
29
+ qrcode.as_png(
30
+ border_modules: quiet_area_size.to_i,
31
+ module_px_size: 1
32
+ ).resample_nearest_neighbor(width_px.to_i, height_px.to_i)
33
+ rescue StandardError => e
34
+ raise if e.is_a?(SmartIdRuby::Errors::RequestSetupError)
35
+
36
+ raise SmartIdRuby::Errors::RequestSetupError, "Unable to create QR-code: #{e.message}"
37
+ end
38
+
39
+ def convert_to_data_uri(image, file_format = DEFAULT_FILE_FORMAT)
40
+ format = file_format.to_s.downcase
41
+ image_bytes = image_to_bytes(image, format)
42
+ encoded = Base64.strict_encode64(image_bytes)
43
+ "data:image/#{format};base64,#{encoded}"
44
+ end
45
+
46
+ private
47
+
48
+ def validate_data!(data)
49
+ return unless data.nil? || data.to_s.empty?
50
+
51
+ raise SmartIdRuby::Errors::RequestSetupError, "Provided data cannot be empty"
52
+ end
53
+
54
+ def image_to_bytes(image, file_format)
55
+ if image.respond_to?(:to_datastream)
56
+ image.to_datastream.to_s
57
+ elsif image.is_a?(String)
58
+ image
59
+ else
60
+ raise SmartIdRuby::Errors::RequestSetupError, "Unable to generate QR-code as #{file_format}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end