Class: CertificateUtility

Inherits:
Object
  • Object
show all
Defined in:
lib/AuthenticationSDK/util/CertificateUtility.rb

Class Method Summary collapse

Class Method Details

.convert_key_to_JWK(keyValue, password = nil) ⇒ Object



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/AuthenticationSDK/util/CertificateUtility.rb', line 147

def self.convert_key_to_JWK(keyValue, password=nil)
  if !keyValue.nil?
    case keyValue
    when String
      begin
        if keyValue.encoding == Encoding::UTF_8
          # This is for PEM formatted string

          encrypted = keyValue.include?('BEGIN ENCRYPTED PRIVATE KEY')
          pkey = nil

          key = begin
            if encrypted
              if password.nil? || password.empty?
                raise ArgumentError, "Encrypted PEM detected, but no password was provided."
              end
              pkey = OpenSSL::PKey.read(keyValue, password)
            else
              # Try without password first

              pkey = OpenSSL::PKey.read(keyValue)
            end
          rescue OpenSSL::PKey::PKeyError
            # If initial attempt failed and a password was provided, retry with password

            if !password.nil? && !password.empty?
              begin
                pkey = OpenSSL::PKey.read(keyValue, password)
              rescue OpenSSL::PKey::PKeyError => e
                raise "Failed to load PEM private key. Incorrect password or corrupted/unsupported format. OpenSSL: #{e.message}"
              end
            else
              raise "Failed to load PEM private key. Invalid key format or password required."
            end
          end
          keyValue = JOSE::JWK.from_key(pkey)
        else
          # This is for P12 formatted string

          begin
            if !password.nil? && !password.empty?
              pkey = OpenSSL::PKCS12.new(keyValue, password)
            else
              pkey = OpenSSL::PKCS12.new(keyValue)
            end
          rescue OpenSSL::PKCS12::PKCS12Error => e
            raise "Could not recover key from P12 data: #{e.message}"
          end

          key = pkey.key
          raise "No private key found in the P12 data" if key.nil?
          keyValue = JOSE::JWK.from_key(key)
        end
      end
    when OpenSSL::PKey::RSA
      keyValue = JOSE::JWK.from_pem(keyValue.to_pem)
    else
      keyValue = JOSE::JWK.from_key(keyValue)
    end
  end
end

.getCertificatesFromPemFile(certificateFilePath) ⇒ Object



10
11
12
13
14
15
16
17
18
19
# File 'lib/AuthenticationSDK/util/CertificateUtility.rb', line 10

def self.getCertificatesFromPemFile(certificateFilePath)
  pem_data = File.read(certificateFilePath)
  certificateList = []

  pem_data.scan(/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m) do |certBlock|
    certificateList << OpenSSL::X509::Certificate.new(certBlock)
  end

  certificateList
end

.load_private_key_from_pem_file(key_file_path, password = nil) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/AuthenticationSDK/util/CertificateUtility.rb', line 117

def self.load_private_key_from_pem_file(key_file_path, password = nil)
  begin
    pem_data = File.binread(key_file_path)

    # OpenSSL::PKey.read supports:

    # - "BEGIN RSA/EC/PRIVATE KEY" (PKCS#1), encrypted or not (Proc-Type/DEK-Info)

    # - "BEGIN PRIVATE KEY" (PKCS#8 unencrypted)

    # - "BEGIN ENCRYPTED PRIVATE KEY" (PKCS#8 encrypted)

    rsa_key = OpenSSL::PKey.read(pem_data, password)
    jwk_private_key = self.convert_key_to_JWK(rsa_key)
    return jwk_private_key
  rescue OpenSSL::PKey::PKeyError => e
    # Missing password for an encrypted PEM

    if pem_data =~ /(BEGIN ENCRYPTED PRIVATE KEY|Proc-Type:\s*4,ENCRYPTED)/ && (password.nil? || password.to_s.empty?)
      raise ArgumentError, "Private key is password protected, but no password was provided."
    end

    # Wrong password (common OpenSSL messages)

    if password && e.message =~ /(bad decrypt|bad password|mac verify failure)/i
      logger&.error("Failed to decrypt PKCS#8 private key - incorrect password provided")
      raise ArgumentError, "Password is incorrect for the encrypted private key. Error: #{e.message}"
    end

    # Unsupported/invalid PEM contents

    raise ArgumentError, "Unsupported PEM object or invalid key: #{e.message}"
  rescue Errno::ENOENT
    raise ArgumentError, "PEM file not found: #{key_file_path}"
  end
end

.read_private_key_from_p12(p12_file_path, password) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/AuthenticationSDK/util/CertificateUtility.rb', line 97

def self.read_private_key_from_p12(p12_file_path, password)
  begin
    # `password` should be a String in Ruby

    raise ArgumentError, "password must be a String" unless password.is_a?(String)

    data = File.binread(p12_file_path)
    pkcs12 = OpenSSL::PKCS12.new(data, password)

    key = pkcs12.key
    raise "No private key found in the P12 file" if key.nil?

    jwk_private_key = self.convert_key_to_JWK(key)
    return jwk_private_key
  rescue OpenSSL::PKCS12::PKCS12Error => e
    raise "Could not recover key from P12: #{e.message}"
  rescue Errno::ENOENT => e
    raise "P12 file not found: #{p12_file_path}"
  end
end

.validateCertificateExpiry(certificate, keyAlias, certificateIdentifier, logConfig) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/AuthenticationSDK/util/CertificateUtility.rb', line 21

def self.validateCertificateExpiry(certificate, keyAlias, certificateIdentifier, logConfig)
  if !CertificateUtility.class_variable_defined?(:@@logger) || @@logger.nil?
    @@logger = Log.new logConfig, "CertificateUtility"
  end
  logger = @@logger.logger

  warning_no_expiry_date = "Certificate does not have expiry date"
  warning_expiring_soon = "Certificate with alias #{keyAlias} is going to expire on %s. Please update the certificate before then."
  warning_expired = "Certificate with alias #{keyAlias} is expired as of %s. Please update the certificate."

  if Constants::MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT == certificateIdentifier
    warning_no_expiry_date = "Certificate for MLE Requests does not have expiry date from mleForRequestPublicCertPath in merchant configuration."
    warning_expiring_soon = "Certificate for MLE Requests with alias #{keyAlias} is going to expire on %s. Please update the certificate provided in mleForRequestPublicCertPath in merchant configuration before then."
    warning_expired = "Certificate for MLE Requests with alias #{keyAlias} is expired as of %s. Please update the certificate provided in mleForRequestPublicCertPath in merchant configuration."
  end

  if Constants::MLE_CACHE_IDENTIFIER_FOR_P12_CERT == certificateIdentifier
    warning_no_expiry_date = "Certificate for MLE Requests does not have expiry date in the P12 file."
    warning_expiring_soon = "Certificate for MLE Requests with alias #{keyAlias} is going to expire on %s. Please update the P12 file before then."
    warning_expired = "Certificate for MLE Requests with alias #{keyAlias} is expired as of %s. Please update the P12 file."
  end

  not_after = certificate.not_after # This returns a Time object in Ruby's OpenSSL

  if not_after.nil?
    logger.warn(warning_no_expiry_date)
  else
    now = Time.now.utc
    if not_after < now
      logger.warn(warning_expired % [not_after])
    else
      time_to_expire = not_after - now
      days_to_expire = (time_to_expire / 86400).to_i
      if days_to_expire < Constants::CERTIFICATE_EXPIRY_DATE_WARNING_DAYS
        logger.warn(warning_expiring_soon % [not_after])
      end
    end
  end
end

.validatePathAndFile(filePath, pathType, logConfig) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/AuthenticationSDK/util/CertificateUtility.rb', line 60

def self.validatePathAndFile(filePath, pathType, logConfig)
  if !CertificateUtility.class_variable_defined?(:@@logger) || @@logger.nil?
    @@logger = Log.new logConfig, "CertificateUtility"
  end
  logger = @@logger.logger

  if filePath.nil? || filePath.strip.empty?
    logger.error("#{pathType} path cannot be null or empty.")
    raise ArgumentError, "#{pathType} path cannot be null or empty."
  end

  normalized_path = filePath.dup
  if File::SEPARATOR == '\\' && normalized_path =~ %r{^/[A-Za-z]:.*}
    normalized_path = normalized_path[1..-1]
  end

  path = normalized_path

  unless File.exist?(path)
    logger.error("#{pathType} does not exist: #{path}")
    raise IOError, "#{pathType} does not exist: #{path}"
  end

  if File.directory?(path)
    logger.error("#{pathType} does not have valid file: #{path}")
    raise IOError, "#{pathType} does not have valid file: #{path}"
  end

  begin
    File.open(path, "rb") {} # Just to check readability

    return path
  rescue => e
    logger.error("#{pathType} is not readable: #{path}")
    raise IOError, "#{pathType} is not readable: #{path}"
  end
end