-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MLE implementation #120
base: master
Are you sure you want to change the base?
MLE implementation #120
Changes from all commits
f916928
516933d
64bef5b
245322e
750c3a8
f5306bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,9 +46,14 @@ def initialize(cybsPropertyObj) | |
@defaultCustomHeaders = cybsPropertyObj['defaultCustomHeaders'] | ||
# Path to client JWE pem file directory | ||
@pemFileDirectory = cybsPropertyObj['pemFileDirectory'] | ||
validateMerchantDetails() | ||
@mleKeyAlias = cybsPropertyObj['mleKeyAlias'] | ||
@useMLEGlobally = cybsPropertyObj['useMLEGlobally'] | ||
@mapToControlMLEonAPI = cybsPropertyObj['mapToControlMLEonAPI'] | ||
validateMerchantDetails | ||
logAllProperties(cybsPropertyObj) | ||
validateMLEConfiguration | ||
end | ||
|
||
#fall back logic | ||
def validateMerchantDetails() | ||
logmessage='' | ||
|
@@ -225,6 +230,44 @@ def validateMerchantDetails() | |
end | ||
end | ||
|
||
def validateMLEConfiguration | ||
unless [true, false].include?(@useMLEGlobally) | ||
err = StandardError.new(Constants::ERROR_PREFIX + "useMLEGlobally must be a boolean") | ||
@log_obj.logger.error(ExceptionHandler.new.new_api_exception err) | ||
raise err | ||
end | ||
if [email protected]? && [email protected]_a?(Hash) | ||
err = StandardError.new(Constants::ERROR_PREFIX + "mapToControlMLEonAPI must be a map") | ||
@log_obj.logger.error(ExceptionHandler.new.new_api_exception err) | ||
raise err | ||
end | ||
|
||
[email protected]? && unless @mleKeyAlias.instance_of? String | ||
(err = StandardError.new(Constants::ERROR_PREFIX + "mleKeyAlias must be a string")) | ||
@log_obj.logger.error(ExceptionHandler.new.new_api_exception err) | ||
raise err | ||
end | ||
if @mleKeyAlias.to_s.empty? | ||
@mleKeyAlias = Constants::DEFAULT_ALIAS_FOR_MLE_CERT | ||
end | ||
|
||
mle_configured = @useMLEGlobally | ||
if [email protected]? && [email protected]? | ||
@mapToControlMLEonAPI.each do |_, value| | ||
unless [true, false].include?(value) && value | ||
mle_configured = true | ||
break | ||
end | ||
end | ||
end | ||
|
||
if mle_configured && !Constants::AUTH_TYPE_JWT.eql?(@authenticationType) | ||
err = StandardError.new(Constants::ERROR_PREFIX + "MLE can only be used with JWT authentication") | ||
@log_obj.logger.error(ExceptionHandler.new.new_api_exception err) | ||
raise | ||
end | ||
end | ||
|
||
def logAllProperties(propertyObj) | ||
merchantConfig = '' | ||
hiddenProperties = (Constants::HIDDEN_MERCHANT_PROPERTIES).split(',') | ||
|
@@ -278,4 +321,7 @@ def logAllProperties(propertyObj) | |
attr_accessor :solutionId | ||
attr_accessor :defaultCustomHeaders | ||
attr_accessor :pemFileDirectory | ||
end | ||
attr_accessor :useMLEGlobally | ||
attr_accessor :mapToControlMLEonAPI | ||
attr_accessor :mleKeyAlias | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
require_relative '../logging/log_factory.rb' | ||
require 'jose' | ||
public | ||
class MLEUtility | ||
@log_obj | ||
def self.check_is_mle_for_API(merchant_config, is_mle_supported_by_cybs_for_api, operation_ids) | ||
is_mle_for_api = false | ||
if is_mle_supported_by_cybs_for_api && merchant_config.useMLEGlobally | ||
is_mle_for_api = true | ||
end | ||
if merchant_config.mapToControlMLEonAPI.nil? && merchant_config.mapToControlMLEonAPI.nil? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why same condition check for 2 times? |
||
operation_ids.each do |operation_id| | ||
if merchant_config.mapToControlMLEonAPI.key?(operation_id) | ||
is_mle_for_api = merchant_config.mapToControlMLEonAPI[operation_id] | ||
break | ||
end | ||
end | ||
end | ||
is_mle_for_api | ||
end | ||
|
||
def encrypt_request_payload(merchant_config, request_payload) | ||
if request_payload.nil? | ||
return nil | ||
end | ||
@log_obj = Log.new(merchant_config.log_config, 'MLEUtility') | ||
@log_obj.logger.info('Encrypting request payload') | ||
@log_obj.logger.debug('LOG_REQUEST_BEFORE_MLE: ' + request_payload) | ||
|
||
|
||
begin | ||
certificate = get_certificate(merchant_config, @log_obj) | ||
validate_certificate(certificate, merchant_config, @log_obj) | ||
serial_number = extract_serial_number_from_certificate(certificate) | ||
if serial_number.nil? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if serial number is null then we can put default serial number from cert as same as implemented in Java. |
||
@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(certificate.public_key) | ||
if jwk.nil? | ||
@log_obj.logger.error('Failed to create JWK object from public key') | ||
raise StandardError.new('Failed to create JWK object from public key') | ||
end | ||
headers = { | ||
'alg' => 'RSA-OAEP-256', | ||
'enc' => 'A256GCM', | ||
'typ' => 'JWT', | ||
'kid' => serial_number, | ||
'iat' => Time.now.to_i | ||
} | ||
jwe = JOSE::JWE.block_encrypt(jwk, request_payload, headers) | ||
|
||
compact_jwe = jwe.compact | ||
@log_obj.logger.debug('LOG_REQUEST_AFTER_MLE: ' + compact_jwe) | ||
return create_request_payload compact_jwe | ||
rescue StandardError => e | ||
@log_obj.logger.error("An error occurred during encryption: #{e.message}") | ||
raise e | ||
end | ||
end | ||
|
||
|
||
def get_certificate(merchant_config, log_obj) | ||
begin | ||
p12_file_path = File.join(merchant_config.keysDirectory, merchant_config.keyFilename + '.p12') | ||
file = File.binread(p12_file_path) | ||
p12_file = OpenSSL::PKCS12.new(file, merchant_config.keyPass) | ||
x5_cert_pem = OpenSSL::X509::Certificate.new(p12_file.certificate) | ||
x5_cert_pem.subject.to_a.each do |attribute| | ||
return x5_cert_pem if attribute[1].include?(merchant_config.mleKeyAlias) | ||
end | ||
p12_file.ca_certs.each do |cert| | ||
cert.subject.to_a.each do |attribute| | ||
return cert if attribute[1].include?(merchant_config.mleKeyAlias) | ||
end | ||
end | ||
rescue OpenSSL::PKCS12::PKCS12Error => e | ||
log_obj.logger.error("Failed to load PKCS12 file: #{e.message}") | ||
raise e | ||
rescue OpenSSL::X509::CertificateError => e | ||
log_obj.logger.error("Failed to create X509 certificate: #{e.message}") | ||
raise e | ||
rescue StandardError => e | ||
log_obj.logger.error("An error occurred while getting the certificate: #{e.message}") | ||
raise e | ||
end | ||
end | ||
|
||
def validate_certificate(certificate, merchant_config, log_obj) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't pass merchant_config, pass only required parameter such as key_alias. IF it is generic function then try to put in certificate utility if available. |
||
if certificate.not_after.nil? | ||
log_obj.logger.warn("Certificate for MLE don't have expiry date.") | ||
end | ||
if certificate.not_after < Time.now | ||
log_obj.logger.error('Certificate with MLE alias ' + merchant_config.mleKeyAlias + ' is expired as of ' + certificate.not_after.to_s + ". Please update p12 file.") | ||
else | ||
time_to_expire = certificate.not_after - Time.now | ||
if time_to_expire < Constants::CERTIFICATE_EXPIRY_DATE_WARNING_DAYS * 24 * 60 * 60 | ||
log_obj.logger.warn('Certificate with MLE alias ' + merchant_config.mleKeyAlias + ' is going to expired on ' + certificate.not_after.to_s + ". Please update p12 file before that.") | ||
end | ||
end | ||
end | ||
|
||
def extract_serial_number_from_certificate(certificate) | ||
return nil 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 | ||
nil | ||
end | ||
|
||
def create_request_payload(compact_jwe) | ||
"{ \"encryptedRequest\": \"#{compact_jwe}\" }" | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check for Map value type should be <String,Boolean>