EE-ID-verification 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/.rubocop.yml +57 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +67 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +201 -0
- data/Makefile +98 -0
- data/README.md +752 -0
- data/Rakefile +13 -0
- data/lib/ee_id_verification/certificate_reader.rb +455 -0
- data/lib/ee_id_verification/models.rb +273 -0
- data/lib/ee_id_verification/version.rb +5 -0
- data/lib/ee_id_verification.rb +118 -0
- data/script/test_id_card.rb +255 -0
- metadata +73 -0
@@ -0,0 +1,273 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module EeIdVerification
|
6
|
+
# Authentication session model for Estonian ID card authentication workflow.
|
7
|
+
#
|
8
|
+
# This class represents an active authentication session during the two-phase
|
9
|
+
# authentication process used by Estonian ID cards. The authentication workflow
|
10
|
+
# is designed to be secure and user-friendly:
|
11
|
+
#
|
12
|
+
# 1. Session Creation: When authentication starts, a session is created with
|
13
|
+
# a unique ID and initial status of :waiting_for_pin
|
14
|
+
# 2. PIN Entry: User provides PIN1 which is verified directly on the card
|
15
|
+
# 3. Session Completion: Upon successful PIN verification, session status
|
16
|
+
# changes to :completed and personal data is extracted
|
17
|
+
#
|
18
|
+
# The session-based approach provides several benefits:
|
19
|
+
# - Separation of concerns between session management and authentication logic
|
20
|
+
# - Timeout handling to prevent indefinite waiting for PIN entry
|
21
|
+
# - State tracking for complex authentication workflows
|
22
|
+
# - Security through session expiration and cleanup
|
23
|
+
#
|
24
|
+
# Session states:
|
25
|
+
# - :waiting_for_pin - Session created, waiting for user PIN input
|
26
|
+
# - :completed - Authentication successful, personal data available
|
27
|
+
# - :failed - Authentication failed (wrong PIN, card error, etc.)
|
28
|
+
# - :expired - Session timed out waiting for PIN entry
|
29
|
+
#
|
30
|
+
# @example Creating and managing a session
|
31
|
+
# session = AuthenticationSession.new(
|
32
|
+
# id: SecureRandom.uuid,
|
33
|
+
# method: :digidoc_local,
|
34
|
+
# status: :waiting_for_pin,
|
35
|
+
# created_at: Time.now,
|
36
|
+
# expires_at: Time.now + 300 # 5 minutes
|
37
|
+
# )
|
38
|
+
#
|
39
|
+
# # Check if session is still valid
|
40
|
+
# if session.expired?
|
41
|
+
# puts "Session expired, please try again"
|
42
|
+
# end
|
43
|
+
class AuthenticationSession
|
44
|
+
# Session attributes for tracking authentication state
|
45
|
+
# @!attribute [rw] id
|
46
|
+
# @return [String] Unique session identifier (typically UUID)
|
47
|
+
# @!attribute [rw] method
|
48
|
+
# @return [Symbol] Authentication method used (:digidoc_local)
|
49
|
+
# @!attribute [rw] status
|
50
|
+
# @return [Symbol] Current session status (:waiting_for_pin, :completed, :failed, :expired)
|
51
|
+
# @!attribute [rw] created_at
|
52
|
+
# @return [Time] When the session was created
|
53
|
+
# @!attribute [rw] expires_at
|
54
|
+
# @return [Time, nil] When the session expires (nil for no expiration)
|
55
|
+
attr_accessor :id, :method, :status, :created_at, :expires_at
|
56
|
+
|
57
|
+
# Initialize a new authentication session with the provided attributes.
|
58
|
+
#
|
59
|
+
# This constructor uses a flexible attribute assignment approach that allows
|
60
|
+
# setting any of the session attributes through a hash. This pattern provides
|
61
|
+
# flexibility for different authentication scenarios while maintaining a
|
62
|
+
# clean interface.
|
63
|
+
#
|
64
|
+
# @param attributes [Hash] Session attributes to set
|
65
|
+
# @option attributes [String] :id Unique session identifier
|
66
|
+
# @option attributes [Symbol] :method Authentication method
|
67
|
+
# @option attributes [Symbol] :status Initial session status
|
68
|
+
# @option attributes [Time] :created_at Session creation time
|
69
|
+
# @option attributes [Time] :expires_at Session expiration time
|
70
|
+
# @example
|
71
|
+
# session = AuthenticationSession.new(
|
72
|
+
# id: "auth-123",
|
73
|
+
# status: :waiting_for_pin,
|
74
|
+
# created_at: Time.now
|
75
|
+
# )
|
76
|
+
def initialize(attributes = {})
|
77
|
+
# Dynamically set attributes using setter methods
|
78
|
+
# This approach ensures only valid attributes are set and leverages
|
79
|
+
# any custom setter logic that might be added in the future
|
80
|
+
attributes.each do |key, value|
|
81
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Check if the authentication session has expired.
|
86
|
+
#
|
87
|
+
# Sessions expire to prevent security issues from long-lived authentication
|
88
|
+
# attempts and to free up system resources. An expired session cannot be
|
89
|
+
# used to complete authentication and should be discarded.
|
90
|
+
#
|
91
|
+
# The expiration check handles cases where no expiration time is set
|
92
|
+
# (nil expires_at means the session never expires automatically).
|
93
|
+
#
|
94
|
+
# @return [Boolean] true if session has expired, false otherwise
|
95
|
+
# @example
|
96
|
+
# session = AuthenticationSession.new(expires_at: Time.now - 60)
|
97
|
+
# puts "Expired!" if session.expired?
|
98
|
+
def expired?
|
99
|
+
expires_at && expires_at < Time.now
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Authentication result model containing the outcome of Estonian ID card authentication.
|
104
|
+
#
|
105
|
+
# This class encapsulates all information returned from a completed (or failed)
|
106
|
+
# authentication attempt. It provides a comprehensive view of the authentication
|
107
|
+
# outcome including:
|
108
|
+
#
|
109
|
+
# - Authentication status and success/failure indicators
|
110
|
+
# - Personal information extracted from the ID card certificate
|
111
|
+
# - Error information if authentication failed
|
112
|
+
# - Convenient methods for checking authentication state
|
113
|
+
#
|
114
|
+
# The result object serves as the primary interface between the authentication
|
115
|
+
# system and consuming applications, providing all necessary information to
|
116
|
+
# make authorization decisions and display user information.
|
117
|
+
#
|
118
|
+
# Personal data fields match those available in Estonian ID card certificates:
|
119
|
+
# - personal_code: 11-digit Estonian personal identification code
|
120
|
+
# - given_name: User's first/given name(s)
|
121
|
+
# - surname: User's family/last name
|
122
|
+
# - country: Country code (always "EE" for Estonian cards)
|
123
|
+
#
|
124
|
+
# @example Successful authentication result
|
125
|
+
# result = AuthenticationResult.new(
|
126
|
+
# session_id: "auth-123",
|
127
|
+
# status: :completed,
|
128
|
+
# authenticated: true,
|
129
|
+
# personal_code: "38001010008",
|
130
|
+
# given_name: "MARI",
|
131
|
+
# surname: "MAASIKAS",
|
132
|
+
# country: "EE"
|
133
|
+
# )
|
134
|
+
#
|
135
|
+
# if result.success?
|
136
|
+
# puts "Welcome, #{result.full_name}!"
|
137
|
+
# log_successful_login(result.personal_code)
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# @example Failed authentication result
|
141
|
+
# result = AuthenticationResult.new(
|
142
|
+
# session_id: "auth-456",
|
143
|
+
# status: :failed,
|
144
|
+
# authenticated: false,
|
145
|
+
# error: "Invalid PIN1"
|
146
|
+
# )
|
147
|
+
#
|
148
|
+
# if result.failure?
|
149
|
+
# puts "Authentication failed: #{result.error}"
|
150
|
+
# end
|
151
|
+
class AuthenticationResult
|
152
|
+
# Result attributes containing authentication outcome and personal data
|
153
|
+
# @!attribute [rw] session_id
|
154
|
+
# @return [String] ID of the session this result belongs to
|
155
|
+
# @!attribute [rw] status
|
156
|
+
# @return [Symbol] Final authentication status (:completed, :failed, etc.)
|
157
|
+
# @!attribute [rw] authenticated
|
158
|
+
# @return [Boolean] Whether authentication was successful
|
159
|
+
# @!attribute [rw] error
|
160
|
+
# @return [String, nil] Error message if authentication failed
|
161
|
+
# @!attribute [rw] personal_code
|
162
|
+
# @return [String, nil] Estonian 11-digit personal identification code
|
163
|
+
# @!attribute [rw] given_name
|
164
|
+
# @return [String, nil] User's first/given name from certificate
|
165
|
+
# @!attribute [rw] surname
|
166
|
+
# @return [String, nil] User's family/last name from certificate
|
167
|
+
# @!attribute [rw] country
|
168
|
+
# @return [String, nil] Country code from certificate (typically "EE")
|
169
|
+
attr_accessor :session_id, :status, :authenticated, :error,
|
170
|
+
:personal_code, :given_name, :surname, :country
|
171
|
+
|
172
|
+
# Initialize a new authentication result with the provided attributes.
|
173
|
+
#
|
174
|
+
# Similar to AuthenticationSession, this uses flexible attribute assignment
|
175
|
+
# to allow setting result fields through a hash. The authenticated flag
|
176
|
+
# defaults to false to ensure secure defaults - authentication must be
|
177
|
+
# explicitly marked as successful.
|
178
|
+
#
|
179
|
+
# @param attributes [Hash] Result attributes to set
|
180
|
+
# @option attributes [String] :session_id Associated session ID
|
181
|
+
# @option attributes [Symbol] :status Authentication status
|
182
|
+
# @option attributes [Boolean] :authenticated Success flag
|
183
|
+
# @option attributes [String] :error Error message for failures
|
184
|
+
# @option attributes [String] :personal_code Estonian personal code
|
185
|
+
# @option attributes [String] :given_name User's first name
|
186
|
+
# @option attributes [String] :surname User's last name
|
187
|
+
# @option attributes [String] :country Country code
|
188
|
+
# @example
|
189
|
+
# result = AuthenticationResult.new(
|
190
|
+
# authenticated: true,
|
191
|
+
# personal_code: "38001010008"
|
192
|
+
# )
|
193
|
+
def initialize(attributes = {})
|
194
|
+
# Set provided attributes using dynamic attribute assignment
|
195
|
+
attributes.each do |key, value|
|
196
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
197
|
+
end
|
198
|
+
|
199
|
+
# Ensure authenticated defaults to false for security
|
200
|
+
# Authentication must be explicitly set to true to be considered successful
|
201
|
+
@authenticated ||= false
|
202
|
+
end
|
203
|
+
|
204
|
+
# Check if the user was successfully authenticated.
|
205
|
+
#
|
206
|
+
# This is the primary method for determining authentication success.
|
207
|
+
# It directly returns the authenticated flag which should only be true
|
208
|
+
# if PIN verification succeeded and personal data was extracted.
|
209
|
+
#
|
210
|
+
# @return [Boolean] true if authentication was successful
|
211
|
+
def authenticated?
|
212
|
+
@authenticated
|
213
|
+
end
|
214
|
+
|
215
|
+
# Check if authentication was successful and no errors occurred.
|
216
|
+
#
|
217
|
+
# This method provides a more comprehensive success check than authenticated?
|
218
|
+
# by also ensuring no error occurred during the process. This catches cases
|
219
|
+
# where authentication might be marked as successful but an error was
|
220
|
+
# encountered during personal data extraction.
|
221
|
+
#
|
222
|
+
# @return [Boolean] true if authenticated and no error present
|
223
|
+
# @example
|
224
|
+
# if result.success?
|
225
|
+
# grant_access(result.personal_code)
|
226
|
+
# else
|
227
|
+
# show_error_message(result.error)
|
228
|
+
# end
|
229
|
+
def success?
|
230
|
+
authenticated? && !error
|
231
|
+
end
|
232
|
+
|
233
|
+
# Check if authentication failed or encountered an error.
|
234
|
+
#
|
235
|
+
# This is the inverse of success? and provides a convenient way to check
|
236
|
+
# for any kind of authentication failure. Useful for error handling and
|
237
|
+
# conditional logic in authentication flows.
|
238
|
+
#
|
239
|
+
# @return [Boolean] true if authentication failed or error occurred
|
240
|
+
def failure?
|
241
|
+
!success?
|
242
|
+
end
|
243
|
+
|
244
|
+
# Get the user's full name by combining given name and surname.
|
245
|
+
#
|
246
|
+
# Estonian ID certificates store names in separate fields (given name and
|
247
|
+
# surname) following international X.509 certificate standards. This method
|
248
|
+
# provides a convenient way to get the complete name for display purposes.
|
249
|
+
#
|
250
|
+
# The method handles various edge cases:
|
251
|
+
# - Missing given name or surname (returns partial name)
|
252
|
+
# - Both names missing (returns nil)
|
253
|
+
# - Extra whitespace (cleaned up by join)
|
254
|
+
#
|
255
|
+
# @return [String, nil] Full name or nil if no name components available
|
256
|
+
# @example
|
257
|
+
# result.given_name = "MARI"
|
258
|
+
# result.surname = "MAASIKAS"
|
259
|
+
# puts result.full_name # => "MARI MAASIKAS"
|
260
|
+
#
|
261
|
+
# result.given_name = nil
|
262
|
+
# result.surname = "MAASIKAS"
|
263
|
+
# puts result.full_name # => "MAASIKAS"
|
264
|
+
def full_name
|
265
|
+
# Return nil if neither name component is available
|
266
|
+
return nil unless given_name || surname
|
267
|
+
|
268
|
+
# Combine available name components, filtering out nil values
|
269
|
+
# compact removes nil values, join combines with space
|
270
|
+
[given_name, surname].compact.join(" ")
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
require_relative "ee_id_verification/version"
|
5
|
+
require_relative "ee_id_verification/certificate_reader"
|
6
|
+
require_relative "ee_id_verification/models"
|
7
|
+
|
8
|
+
# Estonian ID card authentication library.
|
9
|
+
#
|
10
|
+
# Simple Ruby interface for authenticating users with Estonian ID cards
|
11
|
+
# using local card readers. Focuses on DigiDoc Local authentication only.
|
12
|
+
#
|
13
|
+
# ## Basic Usage
|
14
|
+
#
|
15
|
+
# require 'ee_id_verification'
|
16
|
+
#
|
17
|
+
# # Create verifier
|
18
|
+
# verifier = EeIdVerification.new
|
19
|
+
#
|
20
|
+
# # Check if ID card is available
|
21
|
+
# if verifier.available?
|
22
|
+
# # Authenticate user
|
23
|
+
# session = verifier.authenticate
|
24
|
+
# puts "Enter PIN1: "
|
25
|
+
# pin = gets.chomp
|
26
|
+
#
|
27
|
+
# result = verifier.complete_authentication(session, pin)
|
28
|
+
# puts "Welcome #{result.full_name}!" if result.success?
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
module EeIdVerification
|
32
|
+
# Generic error class for the gem.
|
33
|
+
class Error < StandardError; end
|
34
|
+
|
35
|
+
# Main verifier class
|
36
|
+
class Verifier
|
37
|
+
# Initialize the verifier
|
38
|
+
def initialize
|
39
|
+
@reader = CertificateReader.new
|
40
|
+
@sessions = {}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Check if Estonian ID card authentication is available
|
44
|
+
# @return [Boolean] true if card and reader are present
|
45
|
+
def available?
|
46
|
+
@reader.card_present?
|
47
|
+
end
|
48
|
+
|
49
|
+
# Start authentication process
|
50
|
+
# @return [AuthenticationSession] session for PIN completion
|
51
|
+
def authenticate
|
52
|
+
raise Error, "No Estonian ID card detected" unless available?
|
53
|
+
|
54
|
+
session = AuthenticationSession.new(
|
55
|
+
id: SecureRandom.hex(16),
|
56
|
+
method: :digidoc_local,
|
57
|
+
status: :waiting_for_pin,
|
58
|
+
created_at: Time.now,
|
59
|
+
expires_at: Time.now + 300
|
60
|
+
)
|
61
|
+
|
62
|
+
@sessions[session.id] = session
|
63
|
+
session
|
64
|
+
end
|
65
|
+
|
66
|
+
# Complete authentication with PIN
|
67
|
+
# @param session [AuthenticationSession] session from authenticate
|
68
|
+
# @param pin [String] user's PIN1
|
69
|
+
# @return [AuthenticationResult] final result
|
70
|
+
def complete_authentication(session, pin)
|
71
|
+
stored_session = @sessions[session.id]
|
72
|
+
return failed_result(session.id, "Session not found") unless stored_session
|
73
|
+
return failed_result(session.id, "Session expired") if session.expired?
|
74
|
+
|
75
|
+
begin
|
76
|
+
@reader.connect
|
77
|
+
cert = @reader.read_auth_certificate(pin)
|
78
|
+
personal_data = @reader.extract_personal_data(cert)
|
79
|
+
|
80
|
+
@sessions.delete(session.id)
|
81
|
+
|
82
|
+
AuthenticationResult.new(
|
83
|
+
session_id: session.id,
|
84
|
+
status: :completed,
|
85
|
+
authenticated: true,
|
86
|
+
personal_code: personal_data[:personal_code],
|
87
|
+
given_name: personal_data[:given_name],
|
88
|
+
surname: personal_data[:surname],
|
89
|
+
country: personal_data[:country]
|
90
|
+
)
|
91
|
+
rescue StandardError => e
|
92
|
+
failed_result(session.id, e.message)
|
93
|
+
ensure
|
94
|
+
begin
|
95
|
+
@reader.disconnect
|
96
|
+
rescue StandardError
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def failed_result(session_id, error_message)
|
105
|
+
AuthenticationResult.new(
|
106
|
+
session_id: session_id,
|
107
|
+
status: :failed,
|
108
|
+
authenticated: false,
|
109
|
+
error: error_message
|
110
|
+
)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Module-level convenience method
|
115
|
+
def self.new
|
116
|
+
Verifier.new
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,255 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "../lib/ee_id_verification"
|
5
|
+
require "openssl"
|
6
|
+
|
7
|
+
def print_section(title)
|
8
|
+
puts "\n#{title}"
|
9
|
+
puts "=" * title.length
|
10
|
+
end
|
11
|
+
|
12
|
+
def print_subsection(title)
|
13
|
+
puts "\n#{title}:"
|
14
|
+
puts "-" * (title.length + 1)
|
15
|
+
end
|
16
|
+
|
17
|
+
def format_certificate_field(name, value)
|
18
|
+
if value.nil? || value.to_s.strip.empty?
|
19
|
+
puts " #{name}: (not available)"
|
20
|
+
else
|
21
|
+
puts " #{name}: #{value}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_card_hardware_info
|
26
|
+
begin
|
27
|
+
# Get PKCS#11 library and slots directly
|
28
|
+
library = EeIdVerification::CertificateReader.shared_pkcs11_library
|
29
|
+
return { error: "PKCS#11 library not available" } unless library
|
30
|
+
|
31
|
+
slots = library.slots(true)
|
32
|
+
esteid_slots = slots.select do |slot|
|
33
|
+
begin
|
34
|
+
token_info = slot.token_info
|
35
|
+
label = token_info.label.strip
|
36
|
+
manufacturer = token_info.manufacturerID.strip
|
37
|
+
|
38
|
+
label.include?("ESTEID") ||
|
39
|
+
manufacturer.include?("SK") ||
|
40
|
+
label.match?(/PIN[12]/) ||
|
41
|
+
label.include?("Isikutuvastus")
|
42
|
+
rescue
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
return { error: "No Estonian ID card slots found" } if esteid_slots.empty?
|
48
|
+
|
49
|
+
slot = esteid_slots.first
|
50
|
+
token_info = slot.token_info
|
51
|
+
|
52
|
+
{
|
53
|
+
token_label: token_info.label.strip,
|
54
|
+
token_manufacturer: token_info.manufacturerID.strip,
|
55
|
+
token_model: token_info.model.strip,
|
56
|
+
token_serial: token_info.serialNumber.strip,
|
57
|
+
slot_description: "Slot #{slot}"
|
58
|
+
}
|
59
|
+
rescue => e
|
60
|
+
{ error: e.message }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
puts "Estonian ID Card Test & Information Export"
|
65
|
+
puts "=========================================="
|
66
|
+
|
67
|
+
verifier = EeIdVerification.new
|
68
|
+
reader = EeIdVerification::CertificateReader.new
|
69
|
+
|
70
|
+
unless verifier.available?
|
71
|
+
puts "❌ No Estonian ID card detected"
|
72
|
+
puts "Please insert your ID card and ensure reader is connected"
|
73
|
+
exit 1
|
74
|
+
end
|
75
|
+
|
76
|
+
puts "✅ Estonian ID card detected"
|
77
|
+
|
78
|
+
# Get hardware information before authentication
|
79
|
+
print_section("Card Reader & Hardware Information")
|
80
|
+
hardware_info = get_card_hardware_info
|
81
|
+
|
82
|
+
if hardware_info[:error]
|
83
|
+
puts "⚠️ Could not retrieve hardware info: #{hardware_info[:error]}"
|
84
|
+
else
|
85
|
+
format_certificate_field("Token Label", hardware_info[:token_label])
|
86
|
+
format_certificate_field("Token Manufacturer", hardware_info[:token_manufacturer])
|
87
|
+
format_certificate_field("Token Model", hardware_info[:token_model])
|
88
|
+
format_certificate_field("Token Serial Number", hardware_info[:token_serial])
|
89
|
+
format_certificate_field("Slot Description", hardware_info[:slot_description])
|
90
|
+
end
|
91
|
+
|
92
|
+
# Proceed with authentication
|
93
|
+
session = verifier.authenticate
|
94
|
+
puts "\n🔑 Enter your PIN1: "
|
95
|
+
pin = gets.chomp
|
96
|
+
|
97
|
+
result = verifier.complete_authentication(session, pin)
|
98
|
+
|
99
|
+
if result.success?
|
100
|
+
print_section("🎉 Authentication Successful!")
|
101
|
+
|
102
|
+
print_subsection("Basic Identity Information")
|
103
|
+
format_certificate_field("Full Name", result.full_name)
|
104
|
+
format_certificate_field("Given Name", result.given_name)
|
105
|
+
format_certificate_field("Surname", result.surname)
|
106
|
+
format_certificate_field("Personal Code", result.personal_code)
|
107
|
+
format_certificate_field("Country", result.country)
|
108
|
+
|
109
|
+
# Parse personal code for detailed demographics
|
110
|
+
if result.personal_code
|
111
|
+
personal_info = reader.parse_personal_code(result.personal_code)
|
112
|
+
|
113
|
+
if personal_info && !personal_info.empty?
|
114
|
+
print_subsection("Demographic Information")
|
115
|
+
format_certificate_field("Birth Date", personal_info[:birth_date])
|
116
|
+
format_certificate_field("Gender", personal_info[:gender])
|
117
|
+
format_certificate_field("Age", "#{personal_info[:age]} years")
|
118
|
+
|
119
|
+
# Calculate additional demographics
|
120
|
+
if personal_info[:birth_date]
|
121
|
+
birth_year = personal_info[:birth_date].year
|
122
|
+
generation = case birth_year
|
123
|
+
when 1946..1964 then "Baby Boomer"
|
124
|
+
when 1965..1980 then "Generation X"
|
125
|
+
when 1981..1996 then "Millennial"
|
126
|
+
when 1997..2012 then "Generation Z"
|
127
|
+
else "Generation Alpha"
|
128
|
+
end
|
129
|
+
format_certificate_field("Generation", generation)
|
130
|
+
|
131
|
+
# Century calculation
|
132
|
+
century_code = result.personal_code[0].to_i
|
133
|
+
century_info = case century_code
|
134
|
+
when 1, 2 then "19th century (1800-1899)"
|
135
|
+
when 3, 4 then "20th century (1900-1999)"
|
136
|
+
when 5, 6 then "21st century (2000-2099)"
|
137
|
+
when 7, 8 then "22nd century (2100-2199)"
|
138
|
+
else "Unknown century"
|
139
|
+
end
|
140
|
+
format_certificate_field("Birth Century", century_info)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Get detailed certificate information
|
146
|
+
begin
|
147
|
+
reader.connect
|
148
|
+
certificate = reader.read_auth_certificate(pin)
|
149
|
+
|
150
|
+
print_subsection("Certificate Details")
|
151
|
+
format_certificate_field("Certificate Type", "Authentication Certificate (PIN1)")
|
152
|
+
format_certificate_field("Serial Number", certificate.serial.to_s)
|
153
|
+
format_certificate_field("Version", "v#{certificate.version}")
|
154
|
+
format_certificate_field("Valid From", certificate.not_before.strftime("%Y-%m-%d %H:%M:%S UTC"))
|
155
|
+
format_certificate_field("Valid Until", certificate.not_after.strftime("%Y-%m-%d %H:%M:%S UTC"))
|
156
|
+
|
157
|
+
# Check if certificate is still valid
|
158
|
+
now = Time.now
|
159
|
+
if now < certificate.not_before
|
160
|
+
format_certificate_field("Status", "⚠️ Not yet valid")
|
161
|
+
elsif now > certificate.not_after
|
162
|
+
format_certificate_field("Status", "❌ Expired")
|
163
|
+
else
|
164
|
+
days_until_expiry = ((certificate.not_after - now) / (24 * 60 * 60)).to_i
|
165
|
+
format_certificate_field("Status", "✅ Valid (expires in #{days_until_expiry} days)")
|
166
|
+
end
|
167
|
+
|
168
|
+
print_subsection("Certificate Issuer")
|
169
|
+
issuer_parts = certificate.issuer.to_a.to_h { |part| [part[0], part[1]] }
|
170
|
+
format_certificate_field("Common Name", issuer_parts["CN"])
|
171
|
+
format_certificate_field("Organization", issuer_parts["O"])
|
172
|
+
format_certificate_field("Country", issuer_parts["C"])
|
173
|
+
format_certificate_field("Email", issuer_parts["emailAddress"])
|
174
|
+
|
175
|
+
print_subsection("Certificate Subject")
|
176
|
+
subject_parts = certificate.subject.to_a.to_h { |part| [part[0], part[1]] }
|
177
|
+
format_certificate_field("Common Name", subject_parts["CN"])
|
178
|
+
format_certificate_field("Given Name", subject_parts["GN"] || subject_parts["givenName"])
|
179
|
+
format_certificate_field("Surname", subject_parts["SN"] || subject_parts["surname"])
|
180
|
+
format_certificate_field("Serial Number", subject_parts["serialNumber"])
|
181
|
+
format_certificate_field("Country", subject_parts["C"])
|
182
|
+
format_certificate_field("Organization", subject_parts["O"])
|
183
|
+
format_certificate_field("Organizational Unit", subject_parts["OU"])
|
184
|
+
|
185
|
+
print_subsection("Cryptographic Information")
|
186
|
+
public_key = certificate.public_key
|
187
|
+
format_certificate_field("Algorithm", certificate.signature_algorithm)
|
188
|
+
format_certificate_field("Public Key Type", public_key.class.name.split("::").last)
|
189
|
+
|
190
|
+
if public_key.respond_to?(:n) # RSA key
|
191
|
+
key_size = public_key.n.to_s(2).length
|
192
|
+
format_certificate_field("Key Size", "#{key_size} bits")
|
193
|
+
format_certificate_field("Exponent", public_key.e.to_s)
|
194
|
+
end
|
195
|
+
|
196
|
+
print_subsection("Certificate Extensions")
|
197
|
+
certificate.extensions.each do |ext|
|
198
|
+
case ext.oid
|
199
|
+
when "keyUsage"
|
200
|
+
format_certificate_field("Key Usage", ext.value)
|
201
|
+
when "extendedKeyUsage"
|
202
|
+
format_certificate_field("Extended Key Usage", ext.value)
|
203
|
+
when "subjectKeyIdentifier"
|
204
|
+
format_certificate_field("Subject Key ID", ext.value.gsub(":", " "))
|
205
|
+
when "authorityKeyIdentifier"
|
206
|
+
# Parse the authority key identifier
|
207
|
+
aki_value = ext.value.gsub("keyid:", "").split("\n").first
|
208
|
+
format_certificate_field("Authority Key ID", aki_value.gsub(":", " "))
|
209
|
+
when "certificatePolicies"
|
210
|
+
format_certificate_field("Certificate Policies", ext.value)
|
211
|
+
when "crlDistributionPoints"
|
212
|
+
format_certificate_field("CRL Distribution", ext.value.split("\n").join(", "))
|
213
|
+
when "authorityInfoAccess"
|
214
|
+
format_certificate_field("Authority Info Access", ext.value.split("\n").join(", "))
|
215
|
+
else
|
216
|
+
format_certificate_field(ext.oid, ext.value) if ext.value.length < 100
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
print_subsection("Certificate Fingerprints")
|
221
|
+
cert_der = certificate.to_der
|
222
|
+
format_certificate_field("SHA-1", OpenSSL::Digest::SHA1.hexdigest(cert_der).upcase.scan(/.{2}/).join(":"))
|
223
|
+
format_certificate_field("SHA-256", OpenSSL::Digest::SHA256.hexdigest(cert_der).upcase.scan(/.{2}/).join(":"))
|
224
|
+
format_certificate_field("MD5", OpenSSL::Digest::MD5.hexdigest(cert_der).upcase.scan(/.{2}/).join(":"))
|
225
|
+
|
226
|
+
rescue => e
|
227
|
+
puts "\n⚠️ Could not retrieve detailed certificate information: #{e.message}"
|
228
|
+
ensure
|
229
|
+
reader.disconnect rescue nil
|
230
|
+
end
|
231
|
+
|
232
|
+
else
|
233
|
+
puts "\n❌ Authentication failed: #{result.error}"
|
234
|
+
|
235
|
+
# Provide helpful error guidance
|
236
|
+
case result.error
|
237
|
+
when /PIN/i
|
238
|
+
puts "\nTroubleshooting:"
|
239
|
+
puts "- Verify you're using PIN1 (not PIN2 for signing)"
|
240
|
+
puts "- Check if PIN is blocked (3 failed attempts)"
|
241
|
+
puts "- Use DigiDoc4 client to unblock PIN if needed"
|
242
|
+
when /card/i
|
243
|
+
puts "\nTroubleshooting:"
|
244
|
+
puts "- Ensure card is properly inserted"
|
245
|
+
puts "- Check card reader connection"
|
246
|
+
puts "- Try removing and reinserting the card"
|
247
|
+
when /certificate/i
|
248
|
+
puts "\nTroubleshooting:"
|
249
|
+
puts "- Your card may be expired or damaged"
|
250
|
+
puts "- Contact Estonian ID-card support: +372 677 3377"
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
puts "\n" + "=" * 50
|
255
|
+
puts "Test completed at #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|