Skip to content

Commit 54fbb4b

Browse files
committed
Merge remote-tracking branch 'origin/inbound-mle-for-http-signature' into feature/mle-final
2 parents c403bdf + f168e42 commit 54fbb4b

File tree

6 files changed

+343
-71
lines changed

6 files changed

+343
-71
lines changed

src/authentication/core/MerchantConfig.js

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ var Constants = require('../util/Constants');
44
var Logger = require('../logging/Logger');
55
var ApiException = require('../util/ApiException');
66
var LogConfiguration = require('../logging/LogConfiguration');
7+
var path = require('path');
8+
var fs = require('fs');
9+
var path = require('path');
10+
var fs = require('fs');
711

812
/**
913
* This function has all the merchentConfig properties getters and setters methods
@@ -83,6 +87,7 @@ function MerchantConfig(result) {
8387

8488
this.mapToControlMLEonAPI = result.mapToControlMLEonAPI;
8589
this.mleKeyAlias = result.mleKeyAlias; //mleKeyAlias is optional parameter, default value is "CyberSource_SJC_US".
90+
this.mleForRequestPublicCertPath = result.mleForRequestPublicCertPath;
8691

8792
/* Fallback logic*/
8893
this.defaultPropValues();
@@ -426,6 +431,18 @@ MerchantConfig.prototype.setMleKeyAlias = function setMleKeyAlias(mleKeyAlias) {
426431
this.mleKeyAlias = mleKeyAlias;
427432
}
428433

434+
MerchantConfig.prototype.getMleForRequestPublicCertPath = function getMleForRequestPublicCertPath() {
435+
return this.mleForRequestPublicCertPath;
436+
}
437+
438+
MerchantConfig.prototype.setMleForRequestPublicCertPath = function setMleForRequestPublicCertPath(mleForRequestPublicCertPath) {
439+
this.mleForRequestPublicCertPath = mleForRequestPublicCertPath;
440+
}
441+
442+
MerchantConfig.prototype.getP12FilePath = function getP12FilePath() {
443+
return path.resolve(path.join(this.getKeysDirectory(), this.getKeyFileName() + '.p12'));
444+
}
445+
429446
MerchantConfig.prototype.runEnvironmentCheck = function runEnvironmentCheck(logger) {
430447

431448
/*url*/
@@ -575,6 +592,11 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() {
575592
this.keyFilename = this.merchantID;
576593
logger.warn(Constants.KEY_FILE_EMPTY);
577594
}
595+
try {
596+
fs.accessSync(this.getP12FilePath(), fs.constants.R_OK);
597+
} catch (err) {
598+
ApiException.ApiException("Merchant p12 certificate file not found or not readable: " + this.getP12FilePath());
599+
}
578600
}
579601
else if (this.authenticationType.toLowerCase() === Constants.OAUTH)
580602
{
@@ -629,25 +651,46 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() {
629651

630652
//useMLEGlobally check for auth Type
631653
if (this.enableRequestMLEForOptionalApisGlobally === true || this.mapToControlMLEonAPI != null) {
632-
if (this.enableRequestMLEForOptionalApisGlobally === true && this.authenticationType.toLowerCase() !== Constants.JWT) {
633-
ApiException.ApiException("MLE is only supported in JWT auth type", logger);
634-
}
654+
// if (this.enableRequestMLEForOptionalApisGlobally === true && this.authenticationType.toLowerCase() !== Constants.JWT) {
655+
// ApiException.ApiException("MLE is only supported in JWT auth type", logger);
656+
// }
635657

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

640-
if (this.mapToControlMLEonAPI != null && Object.keys(this.mapToControlMLEonAPI).length !== 0) {
641-
var hasTrueValue = false;
642-
for (const[key, value] of Object.entries(this.mapToControlMLEonAPI)) {
643-
if (value === true) {
644-
hasTrueValue = true;
645-
break;
646-
}
647-
}
648-
if (hasTrueValue && this.authenticationType.toLowerCase() !== Constants.JWT) {
649-
ApiException.ApiException("MLE is only supported in JWT auth type", logger);
650-
}
662+
// if (this.mapToControlMLEonAPI != null && Object.keys(this.mapToControlMLEonAPI).length !== 0) {
663+
// var hasTrueValue = false;
664+
// for (const[key, value] of Object.entries(this.mapToControlMLEonAPI)) {
665+
// if (value === true) {
666+
// hasTrueValue = true;
667+
// break;
668+
// }
669+
// }
670+
// if (hasTrueValue && this.authenticationType.toLowerCase() !== Constants.JWT) {
671+
// ApiException.ApiException("MLE is only supported in JWT auth type", logger);
672+
// }
673+
// }
674+
}
675+
if (this.mleForRequestPublicCertPath) {
676+
// First check if the file exists and is readable
677+
try {
678+
fs.accessSync(this.mleForRequestPublicCertPath, fs.constants.R_OK);
679+
} catch (err) {
680+
const errorType = err.code === 'ENOENT' ? 'does not exist' : 'is not readable';
681+
ApiException.ApiException(`mleForRequestPublicCertPath file ${errorType}: ${this.mleForRequestPublicCertPath} (${err.message})`, logger);
682+
}
683+
684+
let stats;
685+
try {
686+
stats = fs.statSync(this.mleForRequestPublicCertPath);
687+
} catch (err) {
688+
ApiException.ApiException(`Error checking file stats for mleForRequestPublicCertPath: ${this.mleForRequestPublicCertPath} (${err.message})`, logger);
689+
}
690+
691+
// Check if it's a file
692+
if (stats.isFile() === false) {
693+
ApiException.ApiException(`mleForRequestPublicCertPath is not a file: ${this.mleForRequestPublicCertPath}`, logger);
651694
}
652695
}
653696

src/authentication/logging/SensitiveDataTags.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ exports.getSensitiveDataTags = function () {
3434
tags.push("signature");
3535
tags.push("prefix");
3636
tags.push("bin");
37+
tags.push("encryptedRequest");
3738

3839
return tags;
3940
}

src/authentication/util/Cache.js

Lines changed: 169 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ var cache = require('memory-cache');
66
var path = require('path');
77
var Constants = require('./Constants');
88
var ApiException = require('./ApiException');
9+
var Logger = require('../logging/Logger');
10+
var Utility = require('./Utility');
11+
12+
function loadP12FileToAsn1(filePath) {
13+
var p12Buffer = fs.readFileSync(filePath);
14+
var p12Der = forge.util.binary.raw.encode(new Uint8Array(p12Buffer));
15+
var p12Asn1 = forge.asn1.fromDer(p12Der);
16+
return p12Asn1;
17+
}
918

1019

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

20-
var filePath = path.resolve(path.join(merchantConfig.getKeysDirectory(), merchantConfig.getKeyFileName() + '.p12'));
29+
var filePath = merchantConfig.getP12FilePath();
2130
if (fs.existsSync(filePath)) {
2231
const stats = fs.statSync(filePath);
2332
const currentFileLastModifiedTime = stats.mtime;
@@ -46,9 +55,7 @@ exports.fetchCachedCertificate = function (merchantConfig, logger) {
4655
//Function to read the file and put values to new cache
4756
function getCertificate(keyPass, filePath, fileLastModifiedTime, logger) {
4857
try {
49-
var p12Buffer = fs.readFileSync(filePath);
50-
var p12Der = forge.util.binary.raw.encode(new Uint8Array(p12Buffer));
51-
var p12Asn1 = forge.asn1.fromDer(p12Der);
58+
var p12Asn1 = loadP12FileToAsn1(filePath);
5259
var certificate = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, keyPass);
5360
cache.put("certificateFromP12File", certificate);
5461
cache.put("certificateLastModifideTimeStamp", fileLastModifiedTime);
@@ -77,3 +84,161 @@ exports.fetchPEMFileForNetworkTokenization = function(merchantConfig) {
7784
}
7885
return cache.get("privateKeyFromPEMFile");
7986
}
87+
88+
89+
exports.getRequestMLECertFromCache = function(merchantConfig) {
90+
var logger = Logger.getLogger(merchantConfig, 'Cache');
91+
var merchantId = merchantConfig.getMerchantID();
92+
var cacheKey = null;
93+
var certificatePath = null;
94+
if (merchantConfig.getMleForRequestPublicCertPath() !== null && merchantConfig.getMleForRequestPublicCertPath() !== undefined) {
95+
cacheKey = merchantId + Constants.MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT;
96+
certificatePath = merchantConfig.getMleForRequestPublicCertPath();
97+
} else if (Constants.JWT === merchantConfig.getAuthenticationType().toLowerCase()) {
98+
certificatePath = merchantConfig.getP12FilePath();
99+
cacheKey = merchantId + Constants.MLE_CACHE_IDENTIFIER_FOR_P12_CERT;
100+
} else {
101+
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.");
102+
return null;
103+
}
104+
return getMLECertBasedOnCacheKey(merchantConfig, cacheKey, certificatePath);
105+
106+
}
107+
108+
function getMLECertBasedOnCacheKey(merchantConfig, cacheKey, certificatePath) {
109+
var cachedMLECert = cache.get(cacheKey);
110+
var logger = Logger.getLogger(merchantConfig, 'Cache');
111+
if (cachedMLECert === null || cachedMLECert === undefined || cachedMLECert.fileLastModifiedTime !== fs.statSync(certificatePath).mtimeMs) {
112+
logger.debug("MLE certificate not found in cache or has been modified. Loading from file: " + certificatePath);
113+
setupMLECache(merchantConfig, cacheKey, certificatePath);
114+
} else {
115+
logger.debug("MLE certificate found in cache for key: " + cacheKey);
116+
}
117+
return cache.get(cacheKey).mleCert;
118+
}
119+
120+
function setupMLECache(merchantConfig, cacheKey, certificateSourcePath) {
121+
var fileLastModifiedTime = fs.statSync(certificateSourcePath).mtimeMs;
122+
var mleCert = null;
123+
if (cacheKey.endsWith(Constants.MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT)) {
124+
mleCert = loadCertificateFromPem(merchantConfig, certificateSourcePath);
125+
}
126+
else if (cacheKey.endsWith(Constants.MLE_CACHE_IDENTIFIER_FOR_P12_CERT)) {
127+
mleCert = loadCertificateFromP12(merchantConfig, certificateSourcePath);
128+
}
129+
cache.put(cacheKey, {
130+
mleCert: mleCert,
131+
fileLastModifiedTime: fileLastModifiedTime
132+
});
133+
validateCertificateExpiry(mleCert, merchantConfig.getMleKeyAlias(), cacheKey, merchantConfig);
134+
}
135+
136+
137+
function loadCertificateFromP12(merchantConfig, certificatePath) {
138+
const logger = Logger.getLogger(merchantConfig, 'Cache');
139+
try {
140+
// Read the P12 file and convert to ASN1
141+
var p12Asn1 = loadP12FileToAsn1(certificatePath);
142+
var p12Cert = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, merchantConfig.getKeyPass());
143+
144+
// Extract the certificate from the P12 container
145+
var certBags = p12Cert.getBags({ bagType: forge.pki.oids.certBag });
146+
if (certBags && certBags[forge.pki.oids.certBag] && certBags[forge.pki.oids.certBag].length > 0) {
147+
// Process all certificates in the P12 file
148+
var certs = [];
149+
for (var i = 0; i < certBags[forge.pki.oids.certBag].length; i++) {
150+
var cert = certBags[forge.pki.oids.certBag][i].cert;
151+
var certPem = forge.pki.certificateToPem(cert);
152+
certs.push(certPem);
153+
}
154+
155+
// Try to find the certificate by alias among all certificates
156+
var mleCert = Utility.findCertificateByAlias(certs, merchantConfig.getMleKeyAlias());
157+
return forge.pki.certificateFromPem(mleCert);
158+
} else {
159+
throw new Error("No certificate found in P12 file");
160+
}
161+
} catch (error) {
162+
ApiException.ApiException(error.message + ". " + Constants.INCORRECT_KEY_PASS, logger);
163+
}
164+
}
165+
166+
function loadCertificateFromPem(merchantConfig, mleCertPath) {
167+
try {
168+
const logger = Logger.getLogger(merchantConfig, 'Cache');
169+
var pemData = fs.readFileSync(mleCertPath, 'utf8');
170+
var certs = Utility.loadPemCertificates(pemData);
171+
var mleCert = null;
172+
if (!certs || certs.length === 0) {
173+
throw new Error("No valid PEM certificates found in the provided path : " + mleCertPath);
174+
}
175+
try {
176+
mleCert = Utility.findCertificateByAlias(certs, merchantConfig.getMleKeyAlias());
177+
178+
} catch (error) {
179+
logger.warn("No certificate found for the specified mleKeyAlias '" + merchantConfig.getMleKeyAlias() + "'. Using the first certificate from file " + mleCertPath + " as the MLE request certificate.");
180+
mleCert = certs[0];
181+
}
182+
// Use node forge to parse the PEM certificate
183+
var forgeCert = forge.pki.certificateFromPem(mleCert);
184+
return forgeCert;
185+
} catch (error) {
186+
ApiException.AuthException("Error occurred while loading MLE certificate from PEM file : " + error.message);
187+
}
188+
}
189+
190+
function validateCertificateExpiry(certificate, keyAlias, cacheKey, merchantConfig) {
191+
var logger = Logger.getLogger(merchantConfig, 'Cache');
192+
193+
var warningMessageForNoExpiryDate = "Certificate does not have expiry date";
194+
var warningMessageForCertificateExpiringSoon = "Certificate with alias {} is going to expire on {}. Please update the certificate before then.";
195+
var warningMessageForExpiredCertificate = "Certificate with alias {} is expired as of {}. Please update the certificate.";
196+
197+
if (cacheKey.endsWith(Constants.MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT)) {
198+
warningMessageForNoExpiryDate = "Certificate for MLE Requests does not have expiry date from mleForRequestPublicCertPath in merchant configuration.";
199+
warningMessageForCertificateExpiringSoon = "Certificate for MLE Requests with alias {} is going to expire on {}. Please update the certificate provided in mleForRequestPublicCertPath in merchant configuration before then.";
200+
warningMessageForExpiredCertificate = "Certificate for MLE Requests with alias {} is expired as of {}. Please update the certificate provided in mleForRequestPublicCertPath in merchant configuration.";
201+
}
202+
203+
if (cacheKey.endsWith(Constants.MLE_CACHE_IDENTIFIER_FOR_P12_CERT)) {
204+
warningMessageForNoExpiryDate = "Certificate for MLE Requests does not have expiry date in the P12 file.";
205+
warningMessageForCertificateExpiringSoon = "Certificate for MLE Requests with alias {} is going to expire on {}. Please update the P12 file before then.";
206+
warningMessageForExpiredCertificate = "Certificate for MLE Requests with alias {} is expired as of {}. Please update the P12 file.";
207+
}
208+
209+
// Get the certificate's notAfter date (expiry date)
210+
var notAfter = null;
211+
try {
212+
// All certificates are now in PEM format
213+
if (certificate.validity && certificate.validity.notAfter) {
214+
notAfter = certificate.validity.notAfter;
215+
} else {
216+
logger.warn("Unknown certificate format. Cannot extract expiry date.");
217+
}
218+
} catch (error) {
219+
logger.warn("Error extracting certificate expiry date: " + error.message);
220+
return;
221+
}
222+
223+
if (!notAfter) {
224+
// Certificate does not have an expiry date
225+
logger.warn(warningMessageForNoExpiryDate);
226+
} else {
227+
var now = new Date();
228+
229+
if (notAfter < now) {
230+
// Certificate is already expired
231+
var expiredMessage = warningMessageForExpiredCertificate.replace("{}", keyAlias).replace("{}", notAfter.toISOString().split('T')[0]);
232+
logger.warn(expiredMessage);
233+
} else {
234+
// Calculate days until expiry
235+
var timeToExpire = notAfter.getTime() - now.getTime();
236+
var daysToExpire = Math.floor(timeToExpire / Constants.FACTOR_DAYS_TO_MILLISECONDS);
237+
238+
if (daysToExpire < Constants.CERTIFICATE_EXPIRY_DATE_WARNING_DAYS) {
239+
var expiringMessage = warningMessageForCertificateExpiringSoon.replace("{}", keyAlias).replace("{}", notAfter.toISOString().split('T')[0]);
240+
logger.warn(expiringMessage);
241+
}
242+
}
243+
}
244+
};

src/authentication/util/Constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ module.exports = {
2929
CERTIFICATE_EXPIRY_DATE_WARNING_DAYS : 90,
3030
FACTOR_DAYS_TO_MILLISECONDS : 24 * 60 * 60 * 1000,
3131
DEFAULT_MLE_ALIAS_FOR_CERT : "CyberSource_SJC_US",
32+
MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT : "_mleCertFromMerchantConfig",
33+
MLE_CACHE_IDENTIFIER_FOR_P12_CERT : "_mleCertFromP12",
3234

3335
OLD_RUN_ENVIRONMENT_CONSTANTS : ["CYBERSOURCE.ENVIRONMENT.SANDBOX", "CYBERSOURCE.ENVIRONMENT.PRODUCTION",
3436
"CYBERSOURCE.IN.ENVIRONMENT.SANDBOX", "CYBERSOURCE.IN.ENVIRONMENT.PRODUCTION"],

0 commit comments

Comments
 (0)