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,345 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "openssl"
5
+
6
+ module SmartIdRuby
7
+ module Validation
8
+ # Validates signature response data.
9
+ class SignatureResponseValidator
10
+ BASE64_PATTERN = /\A[a-zA-Z0-9+\/]+={0,2}\z/.freeze
11
+ CERTIFICATE_LEVEL_ORDER = { "ADVANCED" => 1, "QUALIFIED" => 2, "QSCD" => 2 }.freeze
12
+ SUPPORTED_FLOW_TYPES = ["QR", "Web2App", "App2App", "Notification"].freeze
13
+ SUPPORTED_TRAILER_FIELD = "0xbc"
14
+ SUPPORTED_MASK_GEN_ALGORITHM = "id-mgf1"
15
+ QC_STATEMENTS_EXTENSION_OID = "1.3.6.1.5.5.7.1.3"
16
+ QC_TYPE_STATEMENT_OID = "0.4.0.1862.1.6"
17
+ QUALIFIED_ELECTRONIC_SIGNATURE_OID = "0.4.0.1862.1.6.1"
18
+ SUPPORTED_HASH_ALGORITHMS = {
19
+ "SHA-256" => 32,
20
+ "SHA-384" => 48,
21
+ "SHA-512" => 64,
22
+ "SHA3-256" => 32,
23
+ "SHA3-384" => 48,
24
+ "SHA3-512" => 64
25
+ }.freeze
26
+
27
+ def initialize(certificate_validator: CertificateValidator.new)
28
+ @certificate_validator = certificate_validator
29
+ end
30
+
31
+ def validate(session_status, requested_certificate_level)
32
+ status = normalize_status(session_status)
33
+ if status.nil?
34
+ raise SmartIdRuby::Errors::RequestSetupError, "Parameter 'sessionStatus' is not provided"
35
+ end
36
+ if requested_certificate_level.nil?
37
+ raise SmartIdRuby::Errors::RequestSetupError, "Parameter 'requestedCertificateLevel' is not provided"
38
+ end
39
+ if blank?(status.state)
40
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'state' is empty"
41
+ end
42
+ unless status.complete?
43
+ raise SmartIdRuby::Errors::RequestSetupError, "Session is not complete. State: #{status.state}"
44
+ end
45
+
46
+ validate_session_result(status, requested_certificate_level)
47
+ end
48
+
49
+ private
50
+
51
+ def validate_session_result(status, requested_certificate_level)
52
+ result = status.result
53
+ if result.nil?
54
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'result' is missing"
55
+ end
56
+ if blank?(result.end_result)
57
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'result.endResult' is empty"
58
+ end
59
+
60
+ ErrorResultHandler.handle(result) unless result.end_result == "OK"
61
+
62
+ if blank?(result.document_number)
63
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'result.documentNumber' is empty"
64
+ end
65
+ if blank?(status.interaction_type_used)
66
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'interactionTypeUsed' is empty"
67
+ end
68
+ if blank?(status.signature_protocol)
69
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'signatureProtocol' is empty"
70
+ end
71
+
72
+ certificate_level, certificate = validate_certificate(status.cert, requested_certificate_level)
73
+ validate_signature(status)
74
+
75
+ SmartIdRuby::Models::SignatureResponse.new(
76
+ end_result: result.end_result,
77
+ signature_value_in_base64: status.signature.value,
78
+ algorithm_name: status.signature.signature_algorithm,
79
+ flow_type: status.signature.flow_type,
80
+ certificate: certificate,
81
+ requested_certificate_level: requested_certificate_level,
82
+ certificate_level: certificate_level,
83
+ document_number: result.document_number,
84
+ interaction_flow_used: status.interaction_type_used,
85
+ device_ip_address: status.device_ip_address,
86
+ rsa_ssa_pss_parameters: status.signature.signature_algorithm_parameters
87
+ )
88
+ end
89
+
90
+ def validate_certificate(session_certificate, requested_certificate_level)
91
+ if session_certificate.nil?
92
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'cert' is missing"
93
+ end
94
+ if blank?(session_certificate.value)
95
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'cert.value' is empty"
96
+ end
97
+ if blank?(session_certificate.certificate_level)
98
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'cert.certificateLevel' is empty"
99
+ end
100
+ unless CERTIFICATE_LEVEL_ORDER.key?(session_certificate.certificate_level)
101
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'cert.certificateLevel' has unsupported value"
102
+ end
103
+
104
+ level = session_certificate.certificate_level
105
+ requested_level = requested_certificate_level.to_s
106
+ requested_level = "QUALIFIED" if requested_level.strip.empty?
107
+ if CERTIFICATE_LEVEL_ORDER[level] < CERTIFICATE_LEVEL_ORDER.fetch(requested_level, CERTIFICATE_LEVEL_ORDER["QUALIFIED"])
108
+ raise SmartIdRuby::Errors::CertificateLevelMismatchError
109
+ end
110
+
111
+ certificate = parse_certificate(session_certificate.value, "Signature certificate is invalid")
112
+ @certificate_validator.validate(certificate) if @certificate_validator
113
+ validate_signature_certificate_purpose(certificate, level)
114
+ [level, certificate]
115
+ end
116
+
117
+ def validate_signature_certificate_purpose(certificate, certificate_level)
118
+ key_usage = certificate.extensions.find { |ext| ext.oid == "keyUsage" }&.value.to_s
119
+ unless key_usage.match?(/Non[- ]Repudiation/i)
120
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Certificate does not have Non-Repudiation set in 'KeyUsage' extension"
121
+ end
122
+ return if certificate_level == "ADVANCED"
123
+
124
+ policy_oids = extract_certificate_policy_oids(certificate)
125
+ if policy_oids.empty?
126
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Certificate does not have certificate policy OIDs"
127
+ end
128
+
129
+ required = ["1.3.6.1.4.1.10015.17.2", "0.4.0.194112.1.2"]
130
+ unless (required - policy_oids).empty?
131
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
132
+ "Certificate does not contain required qualified certificate policy OIDs"
133
+ end
134
+ validate_certificate_can_be_used_for_qualified_electronic_signature(certificate)
135
+ end
136
+
137
+ def validate_certificate_can_be_used_for_qualified_electronic_signature(certificate)
138
+ extension = certificate.extensions.find do |ext|
139
+ ["qcStatements", QC_STATEMENTS_EXTENSION_OID].include?(ext.oid)
140
+ end
141
+ if extension.nil?
142
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Certificate does not have 'QCStatements' extension"
143
+ end
144
+
145
+ unless has_qualified_signature_oid?(extension)
146
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
147
+ "Certificate does not have electronic signature OID " \
148
+ "(#{QUALIFIED_ELECTRONIC_SIGNATURE_OID}) in QCStatements extension."
149
+ end
150
+ end
151
+
152
+ def has_qualified_signature_oid?(extension)
153
+ decoded = OpenSSL::ASN1.decode(extension.to_der)
154
+ octet_string = decoded.value.find { |node| node.is_a?(OpenSSL::ASN1::OctetString) }
155
+ return false if octet_string.nil?
156
+
157
+ inner = OpenSSL::ASN1.decode(octet_string.value)
158
+ contains_qc_type_statement_with_esign?(inner)
159
+ rescue OpenSSL::ASN1::ASN1Error
160
+ raise SmartIdRuby::Errors::RequestSetupError, "Unable to parse QCStatements extension"
161
+ end
162
+
163
+ def contains_qc_type_statement_with_esign?(root_node)
164
+ statement_nodes = collect_asn1_sequences(root_node)
165
+ statement_nodes.any? do |statement|
166
+ values = statement.value
167
+ next false unless values.is_a?(Array) && values.first.is_a?(OpenSSL::ASN1::ObjectId)
168
+ next false unless values.first.oid == QC_TYPE_STATEMENT_OID
169
+
170
+ contains_object_id?(statement, QUALIFIED_ELECTRONIC_SIGNATURE_OID)
171
+ end
172
+ end
173
+
174
+ def collect_asn1_sequences(node, acc = [])
175
+ return acc unless node.respond_to?(:value)
176
+
177
+ value = node.value
178
+ if node.is_a?(OpenSSL::ASN1::Sequence) && value.is_a?(Array)
179
+ acc << node
180
+ value.each { |child| collect_asn1_sequences(child, acc) }
181
+ elsif value.is_a?(Array)
182
+ value.each { |child| collect_asn1_sequences(child, acc) }
183
+ end
184
+ acc
185
+ end
186
+
187
+ def contains_object_id?(node, expected_oid)
188
+ return false unless node.respond_to?(:value)
189
+
190
+ value = node.value
191
+ return value == expected_oid if node.is_a?(OpenSSL::ASN1::ObjectId)
192
+ return false unless value.is_a?(Array)
193
+
194
+ value.any? { |child| contains_object_id?(child, expected_oid) }
195
+ end
196
+
197
+ def validate_signature(status)
198
+ unless status.signature_protocol.to_s.casecmp("RAW_DIGEST_SIGNATURE").zero?
199
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'signatureProtocol' has unsupported value"
200
+ end
201
+
202
+ signature = status.signature
203
+ if signature.nil?
204
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'signature' is missing"
205
+ end
206
+
207
+ validate_signature_value(signature.value)
208
+ validate_signature_algorithm_name(signature.signature_algorithm)
209
+ validate_flow_type(signature.flow_type)
210
+ validate_signature_algorithm_parameters(signature.signature_algorithm_parameters)
211
+ end
212
+
213
+ def validate_signature_value(value)
214
+ if blank?(value)
215
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'signature.value' is empty"
216
+ end
217
+ return if BASE64_PATTERN.match?(value)
218
+
219
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
220
+ "Signature session status field 'signature.value' does not have Base64-encoded value"
221
+ end
222
+
223
+ def validate_signature_algorithm_name(signature_algorithm)
224
+ if blank?(signature_algorithm)
225
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field 'signature.signatureAlgorithm' is missing"
226
+ end
227
+ return if signature_algorithm == "rsassa-pss"
228
+
229
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
230
+ "Signature session status field 'signature.signatureAlgorithm' has unsupported value"
231
+ end
232
+
233
+ def validate_flow_type(flow_type)
234
+ if blank?(flow_type)
235
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature session status field `signature.flowType` is empty"
236
+ end
237
+ return if SUPPORTED_FLOW_TYPES.include?(flow_type)
238
+
239
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
240
+ "Signature session status field 'signature.flowType' has unsupported value"
241
+ end
242
+
243
+ def validate_signature_algorithm_parameters(params)
244
+ if params.nil?
245
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
246
+ "Signature session status field 'signature.signatureAlgorithmParameters' is missing"
247
+ end
248
+ hash_algorithm = params.hash_algorithm
249
+ if blank?(hash_algorithm)
250
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
251
+ "Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty"
252
+ end
253
+ unless SUPPORTED_HASH_ALGORITHMS.key?(hash_algorithm)
254
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
255
+ "Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value"
256
+ end
257
+
258
+ mask_gen_algorithm = params.mask_gen_algorithm
259
+ if mask_gen_algorithm.nil?
260
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
261
+ "Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing"
262
+ end
263
+ mask_algorithm = fetch_hash_value(mask_gen_algorithm, :algorithm)
264
+ if blank?(mask_algorithm)
265
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
266
+ "Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty"
267
+ end
268
+ unless mask_algorithm == SUPPORTED_MASK_GEN_ALGORITHM
269
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
270
+ "Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has unsupported value"
271
+ end
272
+ mask_parameters = fetch_hash_value(mask_gen_algorithm, :parameters)
273
+ if mask_parameters.nil?
274
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
275
+ "Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing"
276
+ end
277
+ mask_hash_algorithm = fetch_hash_value(mask_parameters, :hashAlgorithm)
278
+ if blank?(mask_hash_algorithm)
279
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
280
+ "Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty"
281
+ end
282
+ unless SUPPORTED_HASH_ALGORITHMS.key?(mask_hash_algorithm)
283
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
284
+ "Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value"
285
+ end
286
+ unless hash_algorithm == mask_hash_algorithm
287
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
288
+ "Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"
289
+ end
290
+
291
+ if params.salt_length.nil?
292
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
293
+ "Signature session status field 'signature.signatureAlgorithmParameters.saltLength' is missing"
294
+ end
295
+ unless params.salt_length == SUPPORTED_HASH_ALGORITHMS[hash_algorithm]
296
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
297
+ "Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value"
298
+ end
299
+ if blank?(params.trailer_field)
300
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
301
+ "Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty"
302
+ end
303
+ return if params.trailer_field == SUPPORTED_TRAILER_FIELD
304
+
305
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
306
+ "Signature status field `signature.signatureAlgorithmParameters.trailerField` has unsupported value"
307
+ end
308
+
309
+ def parse_certificate(value, error_message)
310
+ decoded = Base64.decode64(value.to_s)
311
+ certificate = OpenSSL::X509::Certificate.new(decoded)
312
+ now = Time.now
313
+ return certificate if certificate.not_before <= now && now <= certificate.not_after
314
+
315
+ raise SmartIdRuby::Errors::UnprocessableResponseError, error_message
316
+ rescue OpenSSL::X509::CertificateError, ArgumentError
317
+ raise SmartIdRuby::Errors::UnprocessableResponseError, error_message
318
+ end
319
+
320
+ def extract_certificate_policy_oids(certificate)
321
+ extension = certificate.extensions.find { |ext| ext.oid == "certificatePolicies" }
322
+ return [] unless extension
323
+
324
+ extension.value.scan(/\b\d+(?:\.\d+)+\b/)
325
+ end
326
+
327
+ def normalize_status(session_status)
328
+ return session_status if session_status.respond_to?(:result)
329
+ return SmartIdRuby::Models::SessionStatus.from_h(session_status) if session_status.is_a?(Hash)
330
+
331
+ nil
332
+ end
333
+
334
+ def fetch_hash_value(payload, key)
335
+ return nil unless payload.respond_to?(:[])
336
+
337
+ payload[key] || payload[key.to_s]
338
+ end
339
+
340
+ def blank?(value)
341
+ value.nil? || value.to_s.strip.empty?
342
+ end
343
+ end
344
+ end
345
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "openssl"
5
+
6
+ module SmartIdRuby
7
+ module Validation
8
+ # Validates RSASSA-PSS signature value in authentication/signature responses.
9
+ class SignatureValueValidator
10
+ def validate(signature_value:, payload:, certificate:, signature_algorithm_parameters:)
11
+ validate_inputs(signature_value, payload, certificate, signature_algorithm_parameters)
12
+
13
+ decoded_signature_value = decode_signature_value(signature_value)
14
+ digest = openssl_digest(signature_algorithm_parameters.hash_algorithm)
15
+ mgf_hash_algorithm = fetch_hash_value(signature_algorithm_parameters.mask_gen_algorithm, :parameters)
16
+ mgf_hash_algorithm = fetch_hash_value(mgf_hash_algorithm, :hashAlgorithm)
17
+ mgf_digest = openssl_digest(mgf_hash_algorithm)
18
+
19
+ valid = certificate.public_key.verify_pss(
20
+ digest,
21
+ decoded_signature_value,
22
+ payload,
23
+ salt_length: signature_algorithm_parameters.salt_length,
24
+ mgf1_hash: mgf_digest
25
+ )
26
+ return if valid
27
+
28
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
29
+ "Provided signature value does not match the calculated signature value"
30
+ rescue OpenSSL::PKey::PKeyError, ArgumentError
31
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Invalid signature algorithm parameters were provided"
32
+ rescue SmartIdRuby::Errors::UnprocessableResponseError
33
+ raise
34
+ rescue StandardError
35
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Signature value validation failed"
36
+ end
37
+
38
+ private
39
+
40
+ def validate_inputs(signature_value, payload, certificate, signature_algorithm_parameters)
41
+ raise SmartIdRuby::Errors::RequestSetupError, "Parameter 'signatureValue' is not provided" if signature_value.nil?
42
+ raise SmartIdRuby::Errors::RequestSetupError, "Parameter 'payload' is not provided" if payload.nil?
43
+ raise SmartIdRuby::Errors::RequestSetupError, "Parameter 'certificate' is not provided" if certificate.nil?
44
+ return unless signature_algorithm_parameters.nil?
45
+
46
+ raise SmartIdRuby::Errors::RequestSetupError, "Parameter 'rsaSsaPssParameters' is not provided"
47
+ end
48
+
49
+ def decode_signature_value(signature_value_in_base64)
50
+ Base64.decode64(signature_value_in_base64)
51
+ rescue ArgumentError
52
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
53
+ "Failed to parse signature value in base64. Incorrectly encoded base64 string: '#{signature_value_in_base64}'"
54
+ end
55
+
56
+ def openssl_digest(hash_algorithm)
57
+ case hash_algorithm
58
+ when "SHA-256" then OpenSSL::Digest.new("SHA256")
59
+ when "SHA-384" then OpenSSL::Digest.new("SHA384")
60
+ when "SHA-512" then OpenSSL::Digest.new("SHA512")
61
+ when "SHA3-256" then OpenSSL::Digest.new("SHA3-256")
62
+ when "SHA3-384" then OpenSSL::Digest.new("SHA3-384")
63
+ when "SHA3-512" then OpenSSL::Digest.new("SHA3-512")
64
+ else
65
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Invalid signature algorithm parameters were provided"
66
+ end
67
+ end
68
+
69
+ def fetch_hash_value(payload, key)
70
+ return nil unless payload.respond_to?(:[])
71
+
72
+ payload[key] || payload[key.to_s]
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartIdRuby
4
+ module Validation
5
+ # Container for trust anchors and CA certificates used in validation.
6
+ class TrustedCaCertStore
7
+ attr_reader :trust_anchors, :trusted_ca_certificates, :ocsp_enabled
8
+
9
+ def initialize(trust_anchors:, trusted_ca_certificates:, ocsp_enabled: false)
10
+ @trust_anchors = Array(trust_anchors).dup.freeze
11
+ @trusted_ca_certificates = Array(trusted_ca_certificates).dup.freeze
12
+ @ocsp_enabled = !!ocsp_enabled
13
+ end
14
+
15
+ def ocsp_enabled?
16
+ ocsp_enabled
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module SmartIdRuby
6
+ # Utility class for calculating verification code from a hash input.
7
+ class VerificationCodeCalculator
8
+ class << self
9
+ # The Verification Code (VC) is computed as:
10
+ # integer(SHA256(data)[-2..-1]) mod 10000
11
+ # where SHA256 rightmost 2 bytes are interpreted as unsigned big-endian.
12
+ def calculate(data)
13
+ validate_data!(data)
14
+
15
+ digest = OpenSSL::Digest::SHA256.digest(data.b)
16
+ rightmost_two_bytes = digest[-2, 2]
17
+ unsigned_value = rightmost_two_bytes.unpack1("n")
18
+
19
+ format("%04d", unsigned_value % 10_000)
20
+ end
21
+
22
+ private
23
+
24
+ def validate_data!(data)
25
+ return if data.is_a?(String) && !data.empty?
26
+
27
+ raise SmartIdRuby::Errors::RequestValidationError, "Parameter 'data' cannot be empty"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartIdRuby
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "logger"
5
+ require "openssl"
6
+ require_relative "smart_id_ruby/configuration"
7
+
8
+ #
9
+ # Top-level namespace for the Smart-ID Ruby client library.
10
+ # Provides configuration, logging, and access to Smart-ID API flows and models.
11
+ module SmartIdRuby
12
+ class Error < StandardError; end
13
+
14
+ extend Configuration
15
+
16
+ class << self
17
+ attr_writer :logger
18
+
19
+ def logger
20
+ @logger ||= if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
21
+ Rails.logger
22
+ else
23
+ Logger.new($stdout).tap do |instance|
24
+ instance.progname = "smart_id"
25
+ instance.level = Logger::WARN
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ require_relative "smart_id_ruby/version"
33
+ require_relative "smart_id_ruby/errors"
34
+ require_relative "smart_id_ruby/client"
35
+ require_relative "smart_id_ruby/rp_challenge"
36
+ require_relative "smart_id_ruby/rp_challenge_generator"
37
+ require_relative "smart_id_ruby/verification_code_calculator"
38
+ require_relative "smart_id_ruby/semantics_identifier"
39
+ require_relative "smart_id_ruby/callback_url"
40
+ require_relative "smart_id_ruby/callback_url_util"
41
+ require_relative "smart_id_ruby/device_link_builder"
42
+ require_relative "smart_id_ruby/qr_code_generator"
43
+ require_relative "smart_id_ruby/device_link_interaction"
44
+ require_relative "smart_id_ruby/notification_interaction"
45
+ require_relative "smart_id_ruby/rest/connector"
46
+ require_relative "smart_id_ruby/rest/session_status_poller"
47
+ require_relative "smart_id_ruby/models/authentication_response"
48
+ require_relative "smart_id_ruby/models/authentication_identity"
49
+ require_relative "smart_id_ruby/models/device_link_session_response"
50
+ require_relative "smart_id_ruby/models/notification_authentication_session_response"
51
+ require_relative "smart_id_ruby/models/notification_certificate_choice_session_response"
52
+ require_relative "smart_id_ruby/models/notification_signature_session_response"
53
+ require_relative "smart_id_ruby/models/signature_response"
54
+ require_relative "smart_id_ruby/models/certificate_choice_response"
55
+ require_relative "smart_id_ruby/models/session_status"
56
+ require_relative "smart_id_ruby/flows/base_builder"
57
+ require_relative "smart_id_ruby/flows/device_link_authentication_session_request_builder"
58
+ require_relative "smart_id_ruby/flows/notification_authentication_session_request_builder"
59
+ require_relative "smart_id_ruby/flows/device_link_signature_session_request_builder"
60
+ require_relative "smart_id_ruby/flows/notification_signature_session_request_builder"
61
+ require_relative "smart_id_ruby/flows/device_link_certificate_choice_session_request_builder"
62
+ require_relative "smart_id_ruby/flows/notification_certificate_choice_session_request_builder"
63
+ require_relative "smart_id_ruby/flows/linked_notification_signature_session_request_builder"
64
+ require_relative "smart_id_ruby/flows/certificate_by_document_number_request_builder"
65
+ require_relative "smart_id_ruby/validation/signature_value_validator"
66
+ require_relative "smart_id_ruby/validation/signature_payload_builder"
67
+ require_relative "smart_id_ruby/validation/trusted_ca_cert_store"
68
+ require_relative "smart_id_ruby/validation/certificate_validator"
69
+ require_relative "smart_id_ruby/validation/authentication_certificate_validator"
70
+ require_relative "smart_id_ruby/validation/authentication_identity_mapper"
71
+ require_relative "smart_id_ruby/validation/error_result_handler"
72
+ require_relative "smart_id_ruby/validation/base_authentication_response_validator"
73
+ require_relative "smart_id_ruby/validation/device_link_authentication_response_validator"
74
+ require_relative "smart_id_ruby/validation/notification_authentication_response_validator"
75
+ require_relative "smart_id_ruby/validation/signature_response_validator"
76
+ require_relative "smart_id_ruby/validation/certificate_choice_response_validator"