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,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module SmartIdRuby
8
+ module Validation
9
+ # Validates X.509 certificate validity period and trust chain.
10
+ class CertificateValidator
11
+ def initialize(trusted_ca_cert_store: nil, use_system_store: true)
12
+ @trusted_ca_cert_store = trusted_ca_cert_store
13
+ @use_system_store = use_system_store
14
+ end
15
+
16
+ def validate(certificate)
17
+ validate_certificate_is_currently_valid(certificate)
18
+ validate_certificate_chain(certificate)
19
+ end
20
+
21
+ private
22
+
23
+ def validate_certificate_is_currently_valid(certificate)
24
+ now = Time.now
25
+ return if certificate.not_before <= now && now <= certificate.not_after
26
+
27
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Certificate is invalid"
28
+ rescue OpenSSL::X509::CertificateError, NoMethodError => e
29
+ logger.error("Certificate is expired or not yet valid: #{certificate_subject(certificate)} (#{e.class}: #{e.message})")
30
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Certificate is invalid"
31
+ end
32
+
33
+ def validate_certificate_chain(certificate)
34
+ store = OpenSSL::X509::Store.new
35
+ store.set_default_paths if @use_system_store
36
+
37
+ trusted_chain = []
38
+ if @trusted_ca_cert_store
39
+ @trusted_ca_cert_store.trust_anchors.each { |cert| store.add_cert(cert) }
40
+ @trusted_ca_cert_store.trusted_ca_certificates.each do |cert|
41
+ store.add_cert(cert)
42
+ trusted_chain << cert
43
+ end
44
+ end
45
+
46
+ store_context = OpenSSL::X509::StoreContext.new(store, certificate, trusted_chain)
47
+ if store_context.verify
48
+ log_validated_chain(store_context)
49
+ validate_ocsp_revocation!(certificate, Array(store_context.chain), store)
50
+ return
51
+ end
52
+
53
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Certificate chain validation failed"
54
+ rescue OpenSSL::X509::StoreError
55
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Certificate chain validation failed"
56
+ end
57
+
58
+ def log_validated_chain(store_context)
59
+ return unless logger.respond_to?(:debug?) && logger.debug?
60
+
61
+ chain = Array(store_context.chain)
62
+ leaf = chain[0]
63
+ intermediate = chain[1]
64
+ trust_anchor = chain.last
65
+ logger.debug(
66
+ "Leaf: #{certificate_common_name(leaf)}, " \
67
+ "Intermediate: #{certificate_common_name(intermediate)}, " \
68
+ "Trust anchor: #{certificate_common_name(trust_anchor)}"
69
+ )
70
+ rescue StandardError
71
+ # Keep certificate validation resilient even if debug chain details are unavailable.
72
+ end
73
+
74
+ def certificate_common_name(certificate)
75
+ return "N/A" unless certificate.respond_to?(:subject) && certificate.subject
76
+
77
+ entry = certificate.subject.to_a.find { |name, _value, _type| name == "CN" }
78
+ entry ? entry[1] : certificate.subject.to_s
79
+ end
80
+
81
+ def certificate_subject(certificate)
82
+ certificate&.subject&.to_s || "unknown"
83
+ end
84
+
85
+ def validate_ocsp_revocation!(certificate, chain, store)
86
+ return unless @trusted_ca_cert_store&.ocsp_enabled?
87
+
88
+ issuer = find_issuer_certificate(certificate, chain)
89
+ ocsp_url = extract_ocsp_url(certificate)
90
+ if issuer.nil? || ocsp_url.nil?
91
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "OCSP validation failed"
92
+ end
93
+
94
+ cert_id = OpenSSL::OCSP::CertificateId.new(certificate, issuer, OpenSSL::Digest::SHA1.new)
95
+ request = OpenSSL::OCSP::Request.new
96
+ request.add_certid(cert_id)
97
+ request.add_nonce
98
+
99
+ response = perform_ocsp_request(ocsp_url, request.to_der)
100
+ unless response.status == OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL
101
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "OCSP responder returned non-success status"
102
+ end
103
+
104
+ basic_response = response.basic
105
+ if basic_response.nil?
106
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "OCSP response does not contain basic response"
107
+ end
108
+
109
+ verify_ocsp_response_signature!(basic_response, store)
110
+
111
+ status_entries = Array(basic_response.status)
112
+ cert_status = status_entries.first&.[](1)
113
+ case cert_status
114
+ when OpenSSL::OCSP::V_CERTSTATUS_GOOD
115
+ logger.debug("OCSP status for certificate is GOOD") if logger.respond_to?(:debug?) && logger.debug?
116
+ when OpenSSL::OCSP::V_CERTSTATUS_REVOKED
117
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Certificate is revoked according to OCSP response"
118
+ else
119
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Certificate OCSP status is unknown"
120
+ end
121
+ rescue OpenSSL::OCSP::OCSPError, OpenSSL::X509::StoreError, SocketError, SystemCallError, Timeout::Error => e
122
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "OCSP validation failed: #{e.message}"
123
+ end
124
+
125
+ def find_issuer_certificate(certificate, chain)
126
+ issuer_subject = certificate&.issuer
127
+ Array(chain).find { |cert| cert != certificate && cert.subject == issuer_subject }
128
+ end
129
+
130
+ def extract_ocsp_url(certificate)
131
+ aia_ext = certificate.extensions.find { |ext| ext.oid == "authorityInfoAccess" }
132
+ return nil if aia_ext.nil?
133
+
134
+ match = aia_ext.value.to_s.match(/OCSP\s*-\s*URI:([^\s,]+)/i)
135
+ match && match[1]
136
+ rescue OpenSSL::X509::ExtensionError, NoMethodError
137
+ nil
138
+ end
139
+
140
+ def perform_ocsp_request(url, body)
141
+ uri = URI.parse(url)
142
+ request = Net::HTTP::Post.new(uri.request_uri)
143
+ request["Content-Type"] = "application/ocsp-request"
144
+ request["Accept"] = "application/ocsp-response"
145
+ request.body = body
146
+
147
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: 10, open_timeout: 10) do |http|
148
+ http.request(request)
149
+ end
150
+ unless response.code.to_i == 200
151
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "OCSP responder returned HTTP #{response.code}"
152
+ end
153
+
154
+ OpenSSL::OCSP::Response.new(response.body)
155
+ end
156
+
157
+ def verify_ocsp_response_signature!(basic_response, store)
158
+ responder_chain = Array(basic_response.certs)
159
+ verified = basic_response.verify(responder_chain, store)
160
+ return if verified
161
+
162
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "OCSP response signature is not valid"
163
+ end
164
+
165
+ def logger
166
+ SmartIdRuby.logger
167
+ end
168
+ end
169
+ end
170
+ 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 device-link authentication session status response and maps it to
9
+ # a typed authentication identity model.
10
+ class DeviceLinkAuthenticationResponseValidator < BaseAuthenticationResponseValidator
11
+ def initialize(signature_value_validator: SignatureValueValidator.new,
12
+ signature_payload_builder: SignaturePayloadBuilder.new,
13
+ certificate_validator: AuthenticationCertificateValidator.new,
14
+ authentication_identity_mapper: AuthenticationIdentityMapper.new)
15
+ @signature_payload_builder = signature_payload_builder
16
+ super(
17
+ signature_value_validator: signature_value_validator,
18
+ certificate_validator: certificate_validator,
19
+ authentication_identity_mapper: authentication_identity_mapper
20
+ )
21
+ end
22
+
23
+ # Validates a completed device-link authentication session status.
24
+ #
25
+ # @param session_status [SmartIdRuby::Models::SessionStatus, Hash]
26
+ # Session status received from Smart-ID RP API. Hash values are mapped to
27
+ # {SmartIdRuby::Models::SessionStatus} before validation.
28
+ # @param authentication_session_request [Hash]
29
+ # Request payload used for initializing the device-link authentication
30
+ # session. Used to validate requested certificate level.
31
+ # @param user_challenge_verifier [String, nil]
32
+ # Callback URL verifier value used in same-device flows. Required only
33
+ # when flow type is Web2App or App2App.
34
+ # @param schema_name [String, nil]
35
+ # RP schema name used in device link generation. Must be provided.
36
+ # @param _brokered_rp_name [String, nil]
37
+ # The brokered RP name, used in the device link.
38
+ #
39
+ # @return [SmartIdRuby::Models::AuthenticationIdentity]
40
+ #
41
+ # @raise [SmartIdRuby::Errors::RequestSetupError]
42
+ # If required input parameters are missing.
43
+ # @raise [SmartIdRuby::Errors::SessionNotCompleteError]
44
+ # If session status state is not COMPLETE.
45
+ # @raise [SmartIdRuby::Errors::SessionEndResultError]
46
+ # If session end result is not OK.
47
+ # @raise [SmartIdRuby::Errors::UnprocessableResponseError]
48
+ # If response contains invalid or unsupported values.
49
+ private
50
+
51
+ def validate_user_challenge(user_challenge_verifier, signature)
52
+ flow_type = signature.flow_type
53
+ return unless %w[Web2App App2App].include?(flow_type)
54
+
55
+ if blank?(user_challenge_verifier)
56
+ raise SmartIdRuby::Errors::RequestSetupError,
57
+ "Parameter 'userChallengeVerifier' must be provided for 'flowType' - #{flow_type}"
58
+ end
59
+ url_user_challenge = Base64.urlsafe_encode64(OpenSSL::Digest::SHA256.digest(user_challenge_verifier), padding: false)
60
+ return if signature.user_challenge == url_user_challenge
61
+
62
+ raise SmartIdRuby::Errors::UnprocessableResponseError,
63
+ "Device link authentication 'signature.userChallenge' does not validate with 'userChallengeVerifier'"
64
+ end
65
+
66
+ def build_signature_payload(session_status, authentication_session_request, schema_name, brokered_rp_name)
67
+ @signature_payload_builder.build(
68
+ session_status: session_status,
69
+ authentication_session_request: authentication_session_request,
70
+ schema_name: schema_name,
71
+ brokered_rp_name: brokered_rp_name
72
+ )
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartIdRuby
4
+ module Validation
5
+ # Handles non-OK session end results and raises mapped exceptions.
6
+ class ErrorResultHandler
7
+ def self.handle(result)
8
+ raise SmartIdRuby::Errors::RequestSetupError, "Parameter 'sessionResult' is not provided" if result.nil?
9
+
10
+ end_result = fetch_value(result, :end_result, :endResult)
11
+ if blank?(end_result)
12
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Session result field 'endResult' is empty"
13
+ end
14
+
15
+ case end_result
16
+ when "USER_REFUSED"
17
+ raise SmartIdRuby::Errors::UserRefusedError
18
+ when "TIMEOUT"
19
+ raise SmartIdRuby::Errors::SessionTimeoutError
20
+ when "DOCUMENT_UNUSABLE"
21
+ raise SmartIdRuby::Errors::DocumentUnusableError
22
+ when "WRONG_VC"
23
+ raise SmartIdRuby::Errors::UserSelectedWrongVerificationCodeError
24
+ when "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP"
25
+ raise SmartIdRuby::Errors::RequiredInteractionNotSupportedByAppError
26
+ when "USER_REFUSED_CERT_CHOICE"
27
+ raise SmartIdRuby::Errors::UserRefusedCertChoiceError
28
+ when "USER_REFUSED_INTERACTION"
29
+ raise_user_refused_interaction_error(result)
30
+ when "PROTOCOL_FAILURE"
31
+ raise SmartIdRuby::Errors::ProtocolFailureError
32
+ when "EXPECTED_LINKED_SESSION"
33
+ raise SmartIdRuby::Errors::ExpectedLinkedSessionError
34
+ when "SERVER_ERROR"
35
+ raise SmartIdRuby::Errors::SmartIdServerError
36
+ when "ACCOUNT_UNUSABLE"
37
+ raise SmartIdRuby::Errors::UserAccountUnusableError
38
+ else
39
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Unexpected session result: #{end_result}"
40
+ end
41
+ end
42
+
43
+ def self.raise_user_refused_interaction_error(result)
44
+ details = fetch_value(result, :details)
45
+ interaction = fetch_value(details, :interaction)
46
+ if blank?(interaction)
47
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Details for refused interaction are missing"
48
+ end
49
+
50
+ case interaction
51
+ when "displayTextAndPIN"
52
+ raise SmartIdRuby::Errors::UserRefusedDisplayTextAndPinError
53
+ when "confirmationMessage"
54
+ raise SmartIdRuby::Errors::UserRefusedConfirmationMessageError
55
+ when "confirmationMessageAndVerificationCodeChoice"
56
+ raise SmartIdRuby::Errors::UserRefusedConfirmationMessageWithVerificationChoiceError
57
+ else
58
+ raise SmartIdRuby::Errors::UnprocessableResponseError, "Unexpected interaction type: #{interaction}"
59
+ end
60
+ end
61
+
62
+ def self.fetch_value(container, *keys)
63
+ return nil if container.nil?
64
+
65
+ keys.each do |key|
66
+ if container.respond_to?(:[])
67
+ value = container[key]
68
+ return value unless value.nil?
69
+
70
+ value = container[key.to_s]
71
+ return value unless value.nil?
72
+ end
73
+
74
+ method_name = key.to_s
75
+ return container.public_send(method_name) if container.respond_to?(method_name)
76
+ end
77
+
78
+ nil
79
+ end
80
+ private_class_method :fetch_value
81
+
82
+ def self.blank?(value)
83
+ value.nil? || value.to_s.strip.empty?
84
+ end
85
+ private_class_method :blank?
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "openssl"
5
+
6
+ module SmartIdRuby
7
+ module Validation
8
+ # Validates notification authentication response data and returns authentication identity.
9
+ class NotificationAuthenticationResponseValidator < BaseAuthenticationResponseValidator
10
+
11
+ def validate(session_status, authentication_session_request, schema_name, brokered_rp_name = nil)
12
+ super(session_status, authentication_session_request, nil, schema_name, brokered_rp_name)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "openssl"
5
+
6
+ module SmartIdRuby
7
+ module Validation
8
+ # Builds ACSP_V2 signature payload used for authentication signature verification.
9
+ class SignaturePayloadBuilder
10
+ def build(session_status:, authentication_session_request:, schema_name:, brokered_rp_name: nil)
11
+ signature_protocol_parameters = fetch_request_value(authentication_session_request, :signatureProtocolParameters)
12
+ rp_challenge = fetch_hash_value(signature_protocol_parameters, :rpChallenge)
13
+ relying_party_name = fetch_request_value(authentication_session_request, :relyingPartyName)
14
+ interactions = fetch_request_value(authentication_session_request, :interactions)
15
+ initial_callback_url = fetch_request_value(authentication_session_request, :initialCallbackUrl)
16
+ flow_type = session_status.signature.flow_type
17
+
18
+ payload_values = [
19
+ schema_name,
20
+ "ACSP_V2",
21
+ session_status.signature.server_random,
22
+ rp_challenge,
23
+ session_status.signature.user_challenge || "",
24
+ to_base64_utf8(relying_party_name),
25
+ blank?(brokered_rp_name) ? "" : to_base64_utf8(brokered_rp_name),
26
+ calculate_interactions_digest(interactions),
27
+ session_status.interaction_type_used,
28
+ flow_type == "QR" ? "" : initial_callback_url,
29
+ flow_type
30
+ ]
31
+ payload_values.join("|")
32
+ end
33
+
34
+ private
35
+
36
+ def calculate_interactions_digest(interactions)
37
+ digest = OpenSSL::Digest::SHA256.digest(interactions.to_s)
38
+ Base64.strict_encode64(digest)
39
+ end
40
+
41
+ def to_base64_utf8(input)
42
+ Base64.strict_encode64(input.to_s.encode("UTF-8"))
43
+ end
44
+
45
+ def fetch_request_value(payload, key)
46
+ return nil unless payload.respond_to?(:[])
47
+
48
+ payload[key] || payload[key.to_s]
49
+ end
50
+
51
+ def fetch_hash_value(payload, key)
52
+ return nil unless payload.respond_to?(:[])
53
+
54
+ payload[key] || payload[key.to_s]
55
+ end
56
+
57
+ def blank?(value)
58
+ value.nil? || value.to_s.strip.empty?
59
+ end
60
+ end
61
+ end
62
+ end