Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lib/AuthenticationSDK/authentication/jwt/JwtToken.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require_relative '../../util/Constants.rb'
require_relative '../../util/ExceptionHandler.rb'
require_relative '../../util/Cache.rb'
require_relative '../../util/MLEUtility.rb'
require_relative '../../authentication/payloadDigest/digest.rb'
require_relative '../../logging/log_factory.rb'

Expand Down Expand Up @@ -67,10 +68,11 @@ def getJwtBody(request_type, gmtDatetime, merchantconfig_obj, isResponseMLEForAp
end

if isResponseMLEForApi
jwtBody = jwtBody + ", \"v-c-response-mle-kid\":\"" + merchantconfig_obj.responseMleKID + "\""
mleKid = MLEUtility.validate_and_auto_extract_response_mle_kid(merchantconfig_obj)
jwtBody = jwtBody + ", \"v-c-response-mle-kid\":\"" + mleKid + "\""
end

jwtBody = jwtBody + "\n}"
end
implements TokenInterface
end
end
9 changes: 7 additions & 2 deletions lib/AuthenticationSDK/core/MerchantConfig.rb
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,15 @@ def validateMLEConfiguration(cybsPropertyObj)
raise err
end

isP12 = false
# If private key file path is provided, validate the file exists
if [email protected]? && [email protected]_s.strip.empty?
begin
CertificateUtility.validatePathAndFile(@responseMlePrivateKeyFilePath, "responseMlePrivateKeyFilePath", @log_config)
ext = File.extname(@responseMlePrivateKeyFilePath).downcase
if ext == '.p12' || ext == '.pfx'
isP12 = true
end
rescue => err
error = StandardError.new(Constants::ERROR_PREFIX + "Invalid responseMlePrivateKeyFilePath : #{err.message}")
@log_obj.logger.error(ExceptionHandler.new.new_api_exception error)
Expand All @@ -440,8 +445,8 @@ def validateMLEConfiguration(cybsPropertyObj)
end

# Validate responseMleKID is provided when response MLE is enabled
if @responseMleKID.nil? || @responseMleKID.to_s.strip.empty?
err = StandardError.new(Constants::ERROR_PREFIX + "Response MLE is enabled but responseMleKID is not provided.")
if !isP12 && (@responseMleKID.nil? || @responseMleKID.to_s.strip.empty?)
err = StandardError.new(Constants::ERROR_PREFIX + "responseMleKID is required when response MLE is enabled for non-P12/PFX files.")
@log_obj.logger.error(ExceptionHandler.new.new_api_exception err)
raise err
end
Expand Down
99 changes: 94 additions & 5 deletions lib/AuthenticationSDK/util/Cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
require 'active_support'
require 'thread'
require_relative 'CacheValue'
require_relative 'CachedMLEKId'
require_relative 'CertificateUtility'
require_relative 'Utility'
require_relative '../util/Constants.rb'
require_relative '../logging/log_factory.rb'
require_relative '../logging/log_configuration.rb'
Expand Down Expand Up @@ -70,8 +72,8 @@ def setupCache(cacheKey, certificateFilePath, merchantConfig)
end

if (cacheKey.end_with?("_JWT"))
privateKey, certificateList = CertificateUtility.getCertificateCollectionAndPrivateKeyFromP12(certificateFilePath, merchantConfig)
jwtCertificate = CertificateUtility.getCertificateBasedOnKeyAlias(certificateList, merchantConfig.keyAlias)
privateKey, certificateList = Utility.getCertificateCollectionAndPrivateKeyFromP12(certificateFilePath, merchantConfig.keyPass)
jwtCertificate = Utility.getCertificateBasedOnKeyAlias(certificateList, merchantConfig.keyAlias)

cacheValue = CacheValue.new(privateKey, jwtCertificate, fileModifiedTime)

Expand All @@ -81,7 +83,7 @@ def setupCache(cacheKey, certificateFilePath, merchantConfig)

if (cacheKey.end_with?(Constants::MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT))
certificateList = CertificateUtility.getCertificatesFromPemFile(certificateFilePath)
mleCertificate = CertificateUtility.getCertificateBasedOnKeyAlias(certificateList, merchantConfig.requestMleKeyAlias)
mleCertificate = Utility.getCertificateBasedOnKeyAlias(certificateList, merchantConfig.requestMleKeyAlias)
if (!mleCertificate)
fileName = File.basename(certificateFilePath)
logger.warn("No certificate found for the specified mle_key_alias '#{merchantConfig.requestMleKeyAlias}'. Using the first certificate from file #{fileName} as the MLE request certificate.")
Expand All @@ -95,8 +97,8 @@ def setupCache(cacheKey, certificateFilePath, merchantConfig)
end

if (cacheKey.end_with?(Constants::MLE_CACHE_IDENTIFIER_FOR_P12_CERT))
privateKey, certificateList = CertificateUtility.getCertificateCollectionAndPrivateKeyFromP12(certificateFilePath, merchantConfig)
mleCertificate = CertificateUtility.getCertificateBasedOnKeyAlias(certificateList, merchantConfig.requestMleKeyAlias)
privateKey, certificateList = Utility.getCertificateCollectionAndPrivateKeyFromP12(certificateFilePath, merchantConfig.keyPass)
mleCertificate = Utility.getCertificateBasedOnKeyAlias(certificateList, merchantConfig.requestMleKeyAlias)
if (!mleCertificate)
fileName = File.basename(certificateFilePath)
logger.error("No certificate found for the specified mle_key_alias '#{merchantConfig.requestMleKeyAlias}' in file #{fileName}.")
Expand Down Expand Up @@ -176,6 +178,93 @@ def getMLEResponsePrivateKeyFromFilePath(merchantConfig)
cachedCertificateInfo ? cachedCertificateInfo.private_key : nil
end

def get_mle_kid_data_from_cache(merchant_config)
cache_key = merchant_config.responseMlePrivateKeyFilePath + Constants::RESPONSE_MLE_P12_PFX_CACHE_IDENTIFIER
file_path = merchant_config.responseMlePrivateKeyFilePath

@@mutex.synchronize do
if !@@cache_obj.exist?(cache_key)
setup_mle_kid_cache(merchant_config)
else
cached_mle_kid = @@cache_obj.read(cache_key)
file_modified_time = File.mtime(file_path).to_i

if cached_mle_kid.nil? || cached_mle_kid.last_modified_timestamp != file_modified_time
if !Cache.class_variable_defined?(:@@logger) || @@logger.nil?
@@logger = Log.new merchant_config.log_config, "Cache"
end
logger = @@logger.logger
logger.info("MLE KID cache outdated or file modified. Refreshing cache for: #{file_path}")
setup_mle_kid_cache(merchant_config)
end
end

return @@cache_obj.read(cache_key)
end
end

private

def setup_mle_kid_cache(merchant_config)
file_path = nil
cache_key = nil

begin
if !Cache.class_variable_defined?(:@@logger) || @@logger.nil?
@@logger = Log.new merchant_config.log_config, "Cache"
end
logger = @@logger.logger

file_path = merchant_config.responseMlePrivateKeyFilePath
cache_key = merchant_config.responseMlePrivateKeyFilePath + Constants::RESPONSE_MLE_P12_PFX_CACHE_IDENTIFIER

# Get certificate from P12 file
_, certificate_list = Utility.getCertificateCollectionAndPrivateKeyFromP12(
file_path,
merchant_config.responseMlePrivateKeyFilePassword
)

merchant_cert = Utility.getCertificateBasedOnKeyAlias(certificate_list, merchant_config.merchantId)

if merchant_cert.nil?
raise StandardError.new("No certificate found for Response MLE Private Key file with merchant ID alias #{merchant_config.merchantId}")
end

# Check if this is a CyberSource-generated P12 file
is_cybersource_p12 = Utility.isP12GeneratedByCyberSource(
file_path,
merchant_config.responseMlePrivateKeyFilePassword,
logger
)

cached_mle_kid = CachedMLEKId.new
cached_mle_kid.last_modified_timestamp = File.mtime(file_path).to_i

if is_cybersource_p12
begin
mle_kid = MLEUtility.extract_serial_number_from_certificate(merchant_cert)
cached_mle_kid.kid = mle_kid
rescue ArgumentError => e
raise StandardError.new("Failed to extract serial number from certificate for Response MLE: #{e.message}")
end
end

@@cache_obj.write(cache_key, cached_mle_kid)

rescue StandardError => e
logger.error("Failed to load MLE KID from Response MLE Private Key file: #{file_path}. Error: #{e.message}")

if !cache_key.nil?
failure_marker = CachedMLEKId.new
failure_marker.kid = nil
failure_marker.last_modified_timestamp = 0
@@cache_obj.write(cache_key, failure_marker)
end
end
end

public

# <b>DEPRECATED:</b> This method has been marked as Deprecated and will be removed in coming releases.
def fetchPEMFileForNetworkTokenization(filePath)
warn("[DEPRECATED] 'fetchPEMFileForNetworkTokenization' method is deprecated and will be removed in coming releases.")
Expand Down
17 changes: 17 additions & 0 deletions lib/AuthenticationSDK/util/CachedMLEKId.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Cache value object to store MLE KID data
class CachedMLEKId
attr_accessor :kid, :last_modified_timestamp

def initialize(kid = nil, last_modified_timestamp = nil)
@kid = kid
@last_modified_timestamp = last_modified_timestamp
end

def to_s
"CachedMLEKId(kid: #{@kid ? 'present' : 'nil'}, last_modified_timestamp: #{@last_modified_timestamp})"
end

def empty?
@kid.nil? && @last_modified_timestamp.nil?
end
end
30 changes: 1 addition & 29 deletions lib/AuthenticationSDK/util/CertificateUtility.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,6 @@
class CertificateUtility
@@logger

def self.getCertificateCollectionAndPrivateKeyFromP12(certificateFilePath, merchantConfig)
if !CertificateUtility.class_variable_defined?(:@@logger) || @@logger.nil?
@@logger = Log.new merchantConfig.log_config, "CertificateUtility"
end
logger = @@logger.logger

p12File = File.binread(certificateFilePath)
p12Object = OpenSSL::PKCS12.new(p12File, merchantConfig.keyPass)

privateKey = OpenSSL::PKey::RSA.new(p12Object.key)

primaryX5Certificate = p12Object.certificate
additionalX5Certificates = p12Object.ca_certs

certificateList = [primaryX5Certificate]
certificateList.concat(additionalX5Certificates) if additionalX5Certificates

return [privateKey, certificateList]
end

def self.getCertificateBasedOnKeyAlias(certificateList, keyAlias)
return nil if certificateList.nil?

certificateList.find do |cert|
cert.subject.to_a.any? { |_, value, _| value.include?(keyAlias) }
end
end

def self.getCertificatesFromPemFile(certificateFilePath)
pem_data = File.read(certificateFilePath)
certificateList = []
Expand Down Expand Up @@ -229,4 +201,4 @@ def self.convert_key_to_JWK(keyValue, password=nil)
end
end
end
end
end
2 changes: 2 additions & 0 deletions lib/AuthenticationSDK/util/Constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,6 @@ class Constants
DEFAULT_KEY_FILE_PATH = File.join(Dir.pwd, "resources")

MLE_CACHE_KEY_IDENTIFIER_FOR_RESPONSE_PRIVATE_KEY = "mleResponsePrivateKeyFromFile"

RESPONSE_MLE_P12_PFX_CACHE_IDENTIFIER = "_responseMleP12Pfx"
end
70 changes: 64 additions & 6 deletions lib/AuthenticationSDK/util/MLEUtility.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,6 @@ def self.encrypt_request_payload(merchantConfig, requestBody)

begin
serial_number = self.extract_serial_number_from_certificate(mleCertificate)
if serial_number.nil?
@log_obj.logger.error('Serial number not found in certificate for MLE')
raise StandardError.new('Serial number not found in MLE certificate')
end

jwk = JOSE::JWK.from_key(mleCertificate.public_key)
if jwk.nil?
Expand All @@ -80,11 +76,12 @@ def self.encrypt_request_payload(merchantConfig, requestBody)
end

def self.extract_serial_number_from_certificate(certificate)
return nil if certificate.subject.to_s.empty? && certificate.issuer.to_s.empty?
raise StandardError.new('Certificate cannot be nil') if certificate.nil?
raise StandardError.new('Certificate subject and issuer cannot both be empty') if certificate.subject.to_s.empty? && certificate.issuer.to_s.empty?
certificate.subject.to_a.each do |attribute|
return attribute[1] if attribute[0].include?('serialNumber')
end
certificate.serial.nil? ? nil : certificate.serial.to_s
raise StandardError.new('Serial number not found in certificate subject')
end

def self.create_request_payload(compact_jwe)
Expand Down Expand Up @@ -184,4 +181,65 @@ def self.get_mle_response_private_key(merchantConfig)
end

private_class_method :get_mle_response_private_key

def self.validate_and_auto_extract_response_mle_kid(merchant_config)
@log_obj ||= Log.new(merchant_config.log_config, 'MLEUtility')

if !merchant_config.responseMlePrivateKey.nil? && !merchant_config.responseMlePrivateKey.to_s.strip.empty?
@log_obj.logger.debug('responseMlePrivateKey is provided directly, using configured responseMleKID')
return merchant_config.responseMleKID
end

@log_obj.logger.debug('Validating responseMleKID for JWT token generation')
cybs_kid = nil
p12_file = false

# File path validity
begin
CertificateUtility.validatePathAndFile(merchant_config.responseMlePrivateKeyFilePath, 'responseMlePrivateKeyFilePath', merchant_config.log_config)
extension = File.extname(merchant_config.responseMlePrivateKeyFilePath).delete_prefix('.').downcase
if extension == 'p12' || extension == 'pfx'
p12_file = true
end
rescue IOError => e
@log_obj.logger.debug('No valid private key file path provided, skipping auto-extraction')
end

if p12_file
@log_obj.logger.debug('P12/PFX file detected, checking if it is a CyberSource certificate')
cached_data = Cache.new.get_mle_kid_data_from_cache(merchant_config)
if !cached_data.nil?
if !cached_data.kid.nil?
# KID present means it's a CyberSource P12, use it
cybs_kid = cached_data.kid
else
# KID is null means either non-CyberSource P12 or extraction failed
@log_obj.logger.debug('Private key file is not a CyberSource generated P12/PFX file, skipping auto-extraction')
end
end
else
@log_obj.logger.debug('Private key file is not a P12/PFX file, skipping auto-extraction')
end

if !cybs_kid.nil?
@log_obj.logger.debug('Successfully auto-extracted responseMleKID from CyberSource P12 certificate')
end

configured_kid = merchant_config.responseMleKID
if cybs_kid.nil? && configured_kid.nil?
raise StandardError.new('responseMleKID is required when response MLE is enabled. ' +
'Could not auto-extract from certificate and no manual configuration provided. ' +
'Please provide responseMleKID explicitly in your configuration.'
)
elsif cybs_kid.nil?
@log_obj.logger.debug('Using manually configured responseMleKID')
return configured_kid
elsif configured_kid.nil?
@log_obj.logger.debug('Using auto-extracted responseMleKID from CyberSource certificate')
return cybs_kid
elsif cybs_kid != configured_kid
@log_obj.logger.warn('Auto-extracted responseMleKID does not match manually configured responseMleKID. Using configured value as preference')
end
return configured_kid
end
end
38 changes: 37 additions & 1 deletion lib/AuthenticationSDK/util/Utility.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'openssl'
require 'base64'
require_relative '../util/Constants.rb'

public

Expand Down Expand Up @@ -32,5 +33,40 @@ def getResponseCodeMessage(responseCode)
end
return tempResponseCodeMessage
end
end

def self.getCertificateBasedOnKeyAlias(certificateList, keyAlias)
return nil if certificateList.nil?

certificateList.find do |cert|
cert.subject.to_a.any? { |_, value, _| value.include?(keyAlias) }
end
end

def self.getCertificateCollectionAndPrivateKeyFromP12(certificateFilePath, keyPass)
p12File = File.binread(certificateFilePath)
p12Object = OpenSSL::PKCS12.new(p12File, keyPass)

privateKey = OpenSSL::PKey::RSA.new(p12Object.key)

primaryX5Certificate = p12Object.certificate
additionalX5Certificates = p12Object.ca_certs

certificateList = [primaryX5Certificate]
certificateList.concat(additionalX5Certificates) if additionalX5Certificates

return [privateKey, certificateList]
end

def self.isP12GeneratedByCyberSource(filePath, password, logger = nil)
begin
_, certificateList = getCertificateCollectionAndPrivateKeyFromP12(filePath, password)

foundCertificate = getCertificateBasedOnKeyAlias(certificateList, Constants::DEFAULT_ALIAS_FOR_MLE_CERT)

return !foundCertificate.nil?
rescue => e
logger&.error("Error while checking if P12 is generated by CyberSource: #{e.message}")
return false
end
end
end