Skip to content

Changes for MLE Request for HTTP Signature #171

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
71 changes: 57 additions & 14 deletions src/authentication/core/MerchantConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ var Constants = require('../util/Constants');
var Logger = require('../logging/Logger');
var ApiException = require('../util/ApiException');
var LogConfiguration = require('../logging/LogConfiguration');
var path = require('path');
var fs = require('fs');
var path = require('path');
var fs = require('fs');

/**
* This function has all the merchentConfig properties getters and setters methods
Expand Down Expand Up @@ -80,6 +84,7 @@ function MerchantConfig(result) {
this.useMLEGlobally = result.useMLEGlobally;
this.mapToControlMLEonAPI = result.mapToControlMLEonAPI;
this.mleKeyAlias = result.mleKeyAlias; //mleKeyAlias is optional parameter, default value is "CyberSource_SJC_US".
this.mleForRequestPublicCertPath = result.mleForRequestPublicCertPath;

/* Fallback logic*/
this.defaultPropValues();
Expand Down Expand Up @@ -415,6 +420,18 @@ MerchantConfig.prototype.setMleKeyAlias = function setMleKeyAlias(mleKeyAlias) {
this.mleKeyAlias = mleKeyAlias;
}

MerchantConfig.prototype.getMleForRequestPublicCertPath = function getMleForRequestPublicCertPath() {
return this.mleForRequestPublicCertPath;
}

MerchantConfig.prototype.setMleForRequestPublicCertPath = function setMleForRequestPublicCertPath(mleForRequestPublicCertPath) {
this.mleForRequestPublicCertPath = mleForRequestPublicCertPath;
}

MerchantConfig.prototype.getP12FilePath = function getP12FilePath() {
return path.resolve(path.join(this.getKeysDirectory(), this.getKeyFileName() + '.p12'));
}

MerchantConfig.prototype.runEnvironmentCheck = function runEnvironmentCheck(logger) {

/*url*/
Expand Down Expand Up @@ -564,6 +581,11 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() {
this.keyFilename = this.merchantID;
logger.warn(Constants.KEY_FILE_EMPTY);
}
try {
fs.accessSync(this.getP12FilePath(), fs.constants.R_OK);
} catch (err) {
ApiException.ApiException("Merchant p12 certificate file not found or not readable: " + this.getP12FilePath());
}
}
else if (this.authenticationType.toLowerCase() === Constants.OAUTH)
{
Expand Down Expand Up @@ -610,25 +632,46 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() {

//useMLEGlobally check for auth Type
if (this.useMLEGlobally === true || this.mapToControlMLEonAPI != null) {
if (this.useMLEGlobally === true && this.authenticationType.toLowerCase() !== Constants.JWT) {
ApiException.ApiException("MLE is only supported in JWT auth type", logger);
}
// if (this.useMLEGlobally === true && this.authenticationType.toLowerCase() !== Constants.JWT) {
// ApiException.ApiException("MLE is only supported in JWT auth type", logger);
// }

if (this.mapToControlMLEonAPI != null && typeof (this.mapToControlMLEonAPI) !== "object") {
ApiException.ApiException("mapToControlMLEonAPI in merchantConfig should be key value pair", logger);
}

if (this.mapToControlMLEonAPI != null && Object.keys(this.mapToControlMLEonAPI).length !== 0) {
var hasTrueValue = false;
for (const[key, value] of Object.entries(this.mapToControlMLEonAPI)) {
if (value === true) {
hasTrueValue = true;
break;
}
}
if (hasTrueValue && this.authenticationType.toLowerCase() !== Constants.JWT) {
ApiException.ApiException("MLE is only supported in JWT auth type", logger);
}
// if (this.mapToControlMLEonAPI != null && Object.keys(this.mapToControlMLEonAPI).length !== 0) {
// var hasTrueValue = false;
// for (const[key, value] of Object.entries(this.mapToControlMLEonAPI)) {
// if (value === true) {
// hasTrueValue = true;
// break;
// }
// }
// if (hasTrueValue && this.authenticationType.toLowerCase() !== Constants.JWT) {
// ApiException.ApiException("MLE is only supported in JWT auth type", logger);
// }
// }
}
if (this.mleForRequestPublicCertPath) {
// First check if the file exists and is readable
try {
fs.accessSync(this.mleForRequestPublicCertPath, fs.constants.R_OK);
} catch (err) {
const errorType = err.code === 'ENOENT' ? 'does not exist' : 'is not readable';
ApiException.ApiException(`mleForRequestPublicCertPath file ${errorType}: ${this.mleForRequestPublicCertPath} (${err.message})`, logger);
}

let stats;
try {
stats = fs.statSync(this.mleForRequestPublicCertPath);
} catch (err) {
ApiException.ApiException(`Error checking file stats for mleForRequestPublicCertPath: ${this.mleForRequestPublicCertPath} (${err.message})`, logger);
}

// Check if it's a file
if (stats.isFile() === false) {
ApiException.ApiException(`mleForRequestPublicCertPath is not a file: ${this.mleForRequestPublicCertPath}`, logger);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/authentication/logging/SensitiveDataTags.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ exports.getSensitiveDataTags = function () {
tags.push("signature");
tags.push("prefix");
tags.push("bin");
tags.push("encryptedRequest");

return tags;
}
173 changes: 169 additions & 4 deletions src/authentication/util/Cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ var cache = require('memory-cache');
var path = require('path');
var Constants = require('./Constants');
var ApiException = require('./ApiException');
var Logger = require('../logging/Logger');
var Utility = require('./Utility');

function loadP12FileToAsn1(filePath) {
var p12Buffer = fs.readFileSync(filePath);
var p12Der = forge.util.binary.raw.encode(new Uint8Array(p12Buffer));
var p12Asn1 = forge.asn1.fromDer(p12Der);
return p12Asn1;
}


/**
Expand All @@ -17,7 +26,7 @@ exports.fetchCachedCertificate = function (merchantConfig, logger) {
var cachedCertificateFromP12File = cache.get("certificateFromP12File");
var cachedLastModifiedTimeStamp = cache.get("certificateLastModifideTimeStamp");

var filePath = path.resolve(path.join(merchantConfig.getKeysDirectory(), merchantConfig.getKeyFileName() + '.p12'));
var filePath = merchantConfig.getP12FilePath();
if (fs.existsSync(filePath)) {
const stats = fs.statSync(filePath);
const currentFileLastModifiedTime = stats.mtime;
Expand Down Expand Up @@ -46,9 +55,7 @@ exports.fetchCachedCertificate = function (merchantConfig, logger) {
//Function to read the file and put values to new cache
function getCertificate(keyPass, filePath, fileLastModifiedTime, logger) {
try {
var p12Buffer = fs.readFileSync(filePath);
var p12Der = forge.util.binary.raw.encode(new Uint8Array(p12Buffer));
var p12Asn1 = forge.asn1.fromDer(p12Der);
var p12Asn1 = loadP12FileToAsn1(filePath);
var certificate = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, keyPass);
cache.put("certificateFromP12File", certificate);
cache.put("certificateLastModifideTimeStamp", fileLastModifiedTime);
Expand Down Expand Up @@ -77,3 +84,161 @@ exports.fetchPEMFileForNetworkTokenization = function(merchantConfig) {
}
return cache.get("privateKeyFromPEMFile");
}


exports.getRequestMLECertFromCache = function(merchantConfig) {
var logger = Logger.getLogger(merchantConfig, 'Cache');
var merchantId = merchantConfig.getMerchantID();
var cacheKey = null;
var certificatePath = null;
if (merchantConfig.getMleForRequestPublicCertPath() !== null && merchantConfig.getMleForRequestPublicCertPath() !== undefined) {
cacheKey = merchantId + Constants.MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT;
certificatePath = merchantConfig.getMleForRequestPublicCertPath();
} else if (Constants.JWT === merchantConfig.getAuthenticationType().toLowerCase()) {
certificatePath = merchantConfig.getP12FilePath();
cacheKey = merchantId + Constants.MLE_CACHE_IDENTIFIER_FOR_P12_CERT;
} else {
logger.debug("The certificate to use for MLE for requests is not provided in the merchant configuration. Please ensure that the certificate path is provided.");
return null;
}
return getMLECertBasedOnCacheKey(merchantConfig, cacheKey, certificatePath);

}

function getMLECertBasedOnCacheKey(merchantConfig, cacheKey, certificatePath) {
var cachedMLECert = cache.get(cacheKey);
var logger = Logger.getLogger(merchantConfig, 'Cache');
if (cachedMLECert === null || cachedMLECert === undefined || cachedMLECert.fileLastModifiedTime !== fs.statSync(certificatePath).mtimeMs) {
logger.debug("MLE certificate not found in cache or has been modified. Loading from file: " + certificatePath);
setupMLECache(merchantConfig, cacheKey, certificatePath);
} else {
logger.debug("MLE certificate found in cache for key: " + cacheKey);
}
return cache.get(cacheKey).mleCert;
}

function setupMLECache(merchantConfig, cacheKey, certificateSourcePath) {
var fileLastModifiedTime = fs.statSync(certificateSourcePath).mtimeMs;
var mleCert = null;
if (cacheKey.endsWith(Constants.MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT)) {
mleCert = loadCertificateFromPem(merchantConfig, certificateSourcePath);
}
else if (cacheKey.endsWith(Constants.MLE_CACHE_IDENTIFIER_FOR_P12_CERT)) {
mleCert = loadCertificateFromP12(merchantConfig, certificateSourcePath);
}
cache.put(cacheKey, {
mleCert: mleCert,
fileLastModifiedTime: fileLastModifiedTime
});
validateCertificateExpiry(mleCert, merchantConfig.getMleKeyAlias(), cacheKey, merchantConfig);
}


function loadCertificateFromP12(merchantConfig, certificatePath) {
const logger = Logger.getLogger(merchantConfig, 'Cache');
try {
// Read the P12 file and convert to ASN1
var p12Asn1 = loadP12FileToAsn1(certificatePath);
var p12Cert = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, merchantConfig.getKeyPass());

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines 142 - 145 are already existing in 51 - 54.

If I need to make a change to this logic in future, I will have to change in both places, which is risky.

Better to create one function to do this.

// Extract the certificate from the P12 container
var certBags = p12Cert.getBags({ bagType: forge.pki.oids.certBag });
if (certBags && certBags[forge.pki.oids.certBag] && certBags[forge.pki.oids.certBag].length > 0) {
// Process all certificates in the P12 file
var certs = [];
for (var i = 0; i < certBags[forge.pki.oids.certBag].length; i++) {
var cert = certBags[forge.pki.oids.certBag][i].cert;
var certPem = forge.pki.certificateToPem(cert);
certs.push(certPem);
}

// Try to find the certificate by alias among all certificates
var mleCert = Utility.findCertificateByAlias(certs, merchantConfig.getMleKeyAlias());
return forge.pki.certificateFromPem(mleCert);
} else {
throw new Error("No certificate found in P12 file");
}
} catch (error) {
ApiException.ApiException(error.message + ". " + Constants.INCORRECT_KEY_PASS, logger);
}
}

function loadCertificateFromPem(merchantConfig, mleCertPath) {
try {
const logger = Logger.getLogger(merchantConfig, 'Cache');
var pemData = fs.readFileSync(mleCertPath, 'utf8');
var certs = Utility.loadPemCertificates(pemData);
var mleCert = null;
if (!certs || certs.length === 0) {
throw new Error("No valid PEM certificates found in the provided path : " + mleCertPath);
}
try {
mleCert = Utility.findCertificateByAlias(certs, merchantConfig.getMleKeyAlias());

} catch (error) {
logger.warn("No certificate found for the specified mleKeyAlias '" + merchantConfig.getMleKeyAlias() + "'. Using the first certificate from file " + mleCertPath + " as the MLE request certificate.");
mleCert = certs[0];
}
// Use node forge to parse the PEM certificate
var forgeCert = forge.pki.certificateFromPem(mleCert);
return forgeCert;
} catch (error) {
ApiException.AuthException("Error occurred while loading MLE certificate from PEM file : " + error.message);
}
}

function validateCertificateExpiry(certificate, keyAlias, cacheKey, merchantConfig) {
var logger = Logger.getLogger(merchantConfig, 'Cache');

var warningMessageForNoExpiryDate = "Certificate does not have expiry date";
var warningMessageForCertificateExpiringSoon = "Certificate with alias {} is going to expire on {}. Please update the certificate before then.";
var warningMessageForExpiredCertificate = "Certificate with alias {} is expired as of {}. Please update the certificate.";

if (cacheKey.endsWith(Constants.MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT)) {
warningMessageForNoExpiryDate = "Certificate for MLE Requests does not have expiry date from mleForRequestPublicCertPath in merchant configuration.";
warningMessageForCertificateExpiringSoon = "Certificate for MLE Requests with alias {} is going to expire on {}. Please update the certificate provided in mleForRequestPublicCertPath in merchant configuration before then.";
warningMessageForExpiredCertificate = "Certificate for MLE Requests with alias {} is expired as of {}. Please update the certificate provided in mleForRequestPublicCertPath in merchant configuration.";
}

if (cacheKey.endsWith(Constants.MLE_CACHE_IDENTIFIER_FOR_P12_CERT)) {
warningMessageForNoExpiryDate = "Certificate for MLE Requests does not have expiry date in the P12 file.";
warningMessageForCertificateExpiringSoon = "Certificate for MLE Requests with alias {} is going to expire on {}. Please update the P12 file before then.";
warningMessageForExpiredCertificate = "Certificate for MLE Requests with alias {} is expired as of {}. Please update the P12 file.";
}

// Get the certificate's notAfter date (expiry date)
var notAfter = null;
try {
// All certificates are now in PEM format
if (certificate.validity && certificate.validity.notAfter) {
notAfter = certificate.validity.notAfter;
} else {
logger.warn("Unknown certificate format. Cannot extract expiry date.");
}
} catch (error) {
logger.warn("Error extracting certificate expiry date: " + error.message);
return;
}

if (!notAfter) {
// Certificate does not have an expiry date
logger.warn(warningMessageForNoExpiryDate);
} else {
var now = new Date();

if (notAfter < now) {
// Certificate is already expired
var expiredMessage = warningMessageForExpiredCertificate.replace("{}", keyAlias).replace("{}", notAfter.toISOString().split('T')[0]);
logger.warn(expiredMessage);
} else {
// Calculate days until expiry
var timeToExpire = notAfter.getTime() - now.getTime();
var daysToExpire = Math.floor(timeToExpire / Constants.FACTOR_DAYS_TO_MILLISECONDS);

if (daysToExpire < Constants.CERTIFICATE_EXPIRY_DATE_WARNING_DAYS) {
var expiringMessage = warningMessageForCertificateExpiringSoon.replace("{}", keyAlias).replace("{}", notAfter.toISOString().split('T')[0]);
logger.warn(expiringMessage);
}
}
}
};
2 changes: 2 additions & 0 deletions src/authentication/util/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ module.exports = {
CERTIFICATE_EXPIRY_DATE_WARNING_DAYS : 90,
FACTOR_DAYS_TO_MILLISECONDS : 24 * 60 * 60 * 1000,
DEFAULT_MLE_ALIAS_FOR_CERT : "CyberSource_SJC_US",
MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT : "_mleCertFromMerchantConfig",
MLE_CACHE_IDENTIFIER_FOR_P12_CERT : "_mleCertFromP12",

OLD_RUN_ENVIRONMENT_CONSTANTS : ["CYBERSOURCE.ENVIRONMENT.SANDBOX", "CYBERSOURCE.ENVIRONMENT.PRODUCTION",
"CYBERSOURCE.IN.ENVIRONMENT.SANDBOX", "CYBERSOURCE.IN.ENVIRONMENT.PRODUCTION"],
Expand Down
Loading