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,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,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"
|