smart-id-ruby-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +14 -0
- data/CHANGELOG.md +13 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +436 -0
- data/Rakefile +12 -0
- data/lib/smart-id-ruby-client.rb +3 -0
- data/lib/smart_id_ruby/callback_url.rb +18 -0
- data/lib/smart_id_ruby/callback_url_util.rb +54 -0
- data/lib/smart_id_ruby/client.rb +124 -0
- data/lib/smart_id_ruby/configuration.rb +184 -0
- data/lib/smart_id_ruby/device_link_builder.rb +301 -0
- data/lib/smart_id_ruby/device_link_interaction.rb +67 -0
- data/lib/smart_id_ruby/errors/certificate_level_mismatch_error.rb +8 -0
- data/lib/smart_id_ruby/errors/document_unusable_error.rb +12 -0
- data/lib/smart_id_ruby/errors/error.rb +8 -0
- data/lib/smart_id_ruby/errors/expected_linked_session_error.rb +15 -0
- data/lib/smart_id_ruby/errors/no_suitable_account_of_requested_type_found_error.rb +10 -0
- data/lib/smart_id_ruby/errors/person_should_view_smart_id_portal_error.rb +8 -0
- data/lib/smart_id_ruby/errors/protocol_failure_error.rb +13 -0
- data/lib/smart_id_ruby/errors/relying_party_account_configuration_error.rb +10 -0
- data/lib/smart_id_ruby/errors/request_setup_error.rb +10 -0
- data/lib/smart_id_ruby/errors/request_validation_error.rb +8 -0
- data/lib/smart_id_ruby/errors/required_interaction_not_supported_by_app_error.rb +13 -0
- data/lib/smart_id_ruby/errors/response_error.rb +8 -0
- data/lib/smart_id_ruby/errors/server_maintenance_error.rb +8 -0
- data/lib/smart_id_ruby/errors/session_end_result_error.rb +15 -0
- data/lib/smart_id_ruby/errors/session_not_complete_error.rb +8 -0
- data/lib/smart_id_ruby/errors/session_not_found_error.rb +8 -0
- data/lib/smart_id_ruby/errors/session_secret_mismatch_error.rb +8 -0
- data/lib/smart_id_ruby/errors/session_timeout_error.rb +12 -0
- data/lib/smart_id_ruby/errors/smart_id_server_error.rb +12 -0
- data/lib/smart_id_ruby/errors/unprocessable_response_error.rb +9 -0
- data/lib/smart_id_ruby/errors/unsupported_client_api_version_error.rb +8 -0
- data/lib/smart_id_ruby/errors/user_account_not_found_error.rb +8 -0
- data/lib/smart_id_ruby/errors/user_account_unusable_error.rb +12 -0
- data/lib/smart_id_ruby/errors/user_refused_cert_choice_error.rb +14 -0
- data/lib/smart_id_ruby/errors/user_refused_confirmation_message_error.rb +13 -0
- data/lib/smart_id_ruby/errors/user_refused_confirmation_message_with_verification_choice_error.rb +13 -0
- data/lib/smart_id_ruby/errors/user_refused_display_text_and_pin_error.rb +13 -0
- data/lib/smart_id_ruby/errors/user_refused_error.rb +12 -0
- data/lib/smart_id_ruby/errors/user_selected_wrong_verification_code_error.rb +13 -0
- data/lib/smart_id_ruby/errors.rb +31 -0
- data/lib/smart_id_ruby/flows/base_builder.rb +90 -0
- data/lib/smart_id_ruby/flows/certificate_by_document_number_request_builder.rb +130 -0
- data/lib/smart_id_ruby/flows/device_link_authentication_session_request_builder.rb +208 -0
- data/lib/smart_id_ruby/flows/device_link_certificate_choice_session_request_builder.rb +112 -0
- data/lib/smart_id_ruby/flows/device_link_signature_session_request_builder.rb +286 -0
- data/lib/smart_id_ruby/flows/linked_notification_signature_session_request_builder.rb +235 -0
- data/lib/smart_id_ruby/flows/notification_authentication_session_request_builder.rb +184 -0
- data/lib/smart_id_ruby/flows/notification_certificate_choice_session_request_builder.rb +96 -0
- data/lib/smart_id_ruby/flows/notification_signature_session_request_builder.rb +272 -0
- data/lib/smart_id_ruby/models/authentication_identity.rb +19 -0
- data/lib/smart_id_ruby/models/authentication_response.rb +38 -0
- data/lib/smart_id_ruby/models/certificate_choice_response.rb +19 -0
- data/lib/smart_id_ruby/models/device_link_session_response.rb +34 -0
- data/lib/smart_id_ruby/models/notification_authentication_session_response.rb +25 -0
- data/lib/smart_id_ruby/models/notification_certificate_choice_session_response.rb +25 -0
- data/lib/smart_id_ruby/models/notification_signature_session_response.rb +29 -0
- data/lib/smart_id_ruby/models/session_status.rb +261 -0
- data/lib/smart_id_ruby/models/signature_response.rb +38 -0
- data/lib/smart_id_ruby/notification_interaction.rb +70 -0
- data/lib/smart_id_ruby/qr_code_generator.rb +65 -0
- data/lib/smart_id_ruby/rest/connector.rb +364 -0
- data/lib/smart_id_ruby/rest/session_status_poller.rb +125 -0
- data/lib/smart_id_ruby/rp_challenge.rb +37 -0
- data/lib/smart_id_ruby/rp_challenge_generator.rb +28 -0
- data/lib/smart_id_ruby/semantics_identifier.rb +35 -0
- data/lib/smart_id_ruby/validation/authentication_certificate_validator.rb +90 -0
- data/lib/smart_id_ruby/validation/authentication_identity_mapper.rb +227 -0
- data/lib/smart_id_ruby/validation/base_authentication_response_validator.rb +304 -0
- data/lib/smart_id_ruby/validation/certificate_choice_response_validator.rb +104 -0
- data/lib/smart_id_ruby/validation/certificate_validator.rb +170 -0
- data/lib/smart_id_ruby/validation/device_link_authentication_response_validator.rb +76 -0
- data/lib/smart_id_ruby/validation/error_result_handler.rb +88 -0
- data/lib/smart_id_ruby/validation/notification_authentication_response_validator.rb +16 -0
- data/lib/smart_id_ruby/validation/signature_payload_builder.rb +62 -0
- data/lib/smart_id_ruby/validation/signature_response_validator.rb +345 -0
- data/lib/smart_id_ruby/validation/signature_value_validator.rb +76 -0
- data/lib/smart_id_ruby/validation/trusted_ca_cert_store.rb +20 -0
- data/lib/smart_id_ruby/verification_code_calculator.rb +31 -0
- data/lib/smart_id_ruby/version.rb +5 -0
- data/lib/smart_id_ruby.rb +76 -0
- metadata +173 -0
|
@@ -0,0 +1,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
|