diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index 49f86dea..dc7c8eaf 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -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 @@ -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(); @@ -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*/ @@ -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) { @@ -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); } } diff --git a/src/authentication/logging/SensitiveDataTags.js b/src/authentication/logging/SensitiveDataTags.js index 181fbec4..37bcf326 100644 --- a/src/authentication/logging/SensitiveDataTags.js +++ b/src/authentication/logging/SensitiveDataTags.js @@ -34,6 +34,7 @@ exports.getSensitiveDataTags = function () { tags.push("signature"); tags.push("prefix"); tags.push("bin"); + tags.push("encryptedRequest"); return tags; } \ No newline at end of file diff --git a/src/authentication/util/Cache.js b/src/authentication/util/Cache.js index 6ee2d240..f18c4c42 100644 --- a/src/authentication/util/Cache.js +++ b/src/authentication/util/Cache.js @@ -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; +} /** @@ -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; @@ -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); @@ -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()); + + // 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); + } + } + } +}; diff --git a/src/authentication/util/Constants.js b/src/authentication/util/Constants.js index b21aad97..46957584 100644 --- a/src/authentication/util/Constants.js +++ b/src/authentication/util/Constants.js @@ -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"], diff --git a/src/authentication/util/MLEUtility.js b/src/authentication/util/MLEUtility.js index 97f3623a..930880b7 100644 --- a/src/authentication/util/MLEUtility.js +++ b/src/authentication/util/MLEUtility.js @@ -5,6 +5,7 @@ const forge = require('node-forge'); const Logger= require('../logging/Logger'); const ApiException= require('./ApiException'); const Constants = require('./Constants'); +const Cache = require('./Cache'); exports.checkIsMLEForAPI = function(merchantConfig, isMLESupportedByCybsForApi, operationId) { //isMLE for an api is false by default @@ -31,54 +32,58 @@ exports.checkIsMLEForAPI = function(merchantConfig, isMLESupportedByCybsForApi, } exports.encryptRequestPayload = function(merchantConfig, requestBody) { - if (requestBody != null) { - var logger = Logger.getLogger(merchantConfig, 'MLEUtility'); - return generateJWEToken(requestBody, logger, merchantConfig).then(token => { - logger.debug(Constants.LOG_REQUEST_BEFORE_MLE + JSON.stringify(requestBody)); - let mleRequest = createMLEJsonRequest(token); - logger.debug(Constants.LOG_REQUEST_AFTER_MLE + JSON.stringify(mleRequest)); - return mleRequest; - }); - } else { - return Promise.resolve(requestBody); + if (requestBody == null) { + return Promise.resolve(requestBody); } -} - -function generateJWEToken(requestBody, logger, merchantConfig) { - //get the MLE cert and verify the expiry of cert - let cert = KeyCertificate.getX509CertificateInCert(merchantConfig, logger, merchantConfig.getMleKeyAlias()); - let isCertExpired = KeyCertificate.verifyIsCertificateExpired(cert, merchantConfig.getMleKeyAlias(), logger); - // if (isCertExpired === true) { - // ApiException.ApiException("Certificate for MLE with alias " + merchantConfig.getMleKeyAlias() + " is expired in " + merchantConfig.getKeyFileName() + ".p12", logger); - // } + + var logger = Logger.getLogger(merchantConfig, 'MLEUtility'); + + //get the MLE cert and verify the expiry of cert + let cert = Cache.getRequestMLECertFromCache(merchantConfig); + if ((cert === null || cert === undefined) && Constants.HTTP == merchantConfig.getAuthenticationType()) { + 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."); + logger.debug("Currently, MLE for requests using HTTP Signature as authentication is not supported by Cybersource. By default, the SDK will fall back to non-encrypted requests."); + return Promise.resolve(requestBody); + } + // let isCertExpired = KeyCertificate.verifyIsCertificateExpired(cert, merchantConfig.getMleKeyAlias(), logger); + // if (isCertExpired === true) { + // ApiException.ApiException("Certificate for MLE with alias " + merchantConfig.getMleKeyAlias() + " is expired in " + merchantConfig.getKeyFileName() + ".p12", logger); + // } - const customHeaders = { - iat: Math.floor(Date.now() / 1000) //epoch time in seconds - }; - const serialNumber = getSerialNumberFromCert(cert, merchantConfig, logger); - const headers = { - alg: "RSA-OAEP-256", - enc: "A256GCM", - cty: "JWT", - kid: serialNumber, - ...customHeaders - }; - if (requestBody !== "{}") { - requestBody = JSON.stringify(requestBody, null, 0); - } - const payload = Buffer.from(requestBody); - const publicKeyInJWK = { - kty: 'RSA', - n: toBase64Url(cert.publicKey.n), - e: toBase64Url(cert.publicKey.e), - }; + const customHeaders = { + iat: Math.floor(Date.now() / 1000) //epoch time in seconds + }; + const serialNumber = getSerialNumberFromCert(cert, merchantConfig, logger); + const headers = { + alg: "RSA-OAEP-256", + enc: "A256GCM", + cty: "JWT", + kid: serialNumber, + ...customHeaders + }; + + let requestBodyStr = requestBody; + if (requestBody !== "{}") { + requestBodyStr = JSON.stringify(requestBody, null, 0); + } + const payload = Buffer.from(requestBodyStr); + const publicKeyInJWK = { + kty: 'RSA', + n: toBase64Url(cert.publicKey.n), + e: toBase64Url(cert.publicKey.e), + }; - return jose.JWE.createEncrypt({ format: 'compact', fields: headers }, { key: publicKeyInJWK, header: { kid: serialNumber } }) - .update(payload) - .final() - .then(result => { - return result; - }); + return jose.JWE.createEncrypt({ format: 'compact', fields: headers }, { key: publicKeyInJWK, header: { kid: serialNumber } }) + .update(payload) + .final() + .then(token => { + logger.debug(Constants.LOG_REQUEST_BEFORE_MLE + JSON.stringify(requestBody)); + const mleRequest = { + encryptedRequest: token + }; + logger.debug(Constants.LOG_REQUEST_AFTER_MLE + JSON.stringify(mleRequest)); + return mleRequest; + }); } function toBase64Url(bi) { @@ -87,13 +92,6 @@ function toBase64Url(bi) { return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } -function createMLEJsonRequest(jweToken) { - const mleJson = { - encryptedRequest: jweToken - }; - return mleJson; -} - function getSerialNumberFromCert(cert, merchantConfig, logger) { if (!cert.subject || !cert.subject.attributes) { throw new Error("Subject or attributes are missing in MLE cert"); diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index 83491fe0..a813bfd3 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -1,5 +1,6 @@ 'use strict' +var ApiException = require('./ApiException'); var Constants = require('./Constants') exports.getResponseCodeMessage = function (responseCode) { @@ -51,4 +52,66 @@ exports.isJsonString = function(jsonString){ } catch (e) { return false; } -} \ No newline at end of file +} + +exports.loadPemCertificates = function (pemCertificatePath) { + if (pemCertificatePath === null || pemCertificatePath === undefined) { + return null; + } + const certs = pemCertificatePath.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/gm); + return certs; +} + + +exports.findCertificateByAlias = function (certs, keyAlias) { + if (certs === null || certs === undefined || keyAlias === null || keyAlias === undefined) { + return null; + } + + if (!Array.isArray(certs)) { + ApiException.AuthException("Invalid certificate format. Expected an array of certificates."); + } + + const forge = require('node-forge'); + + + try { + // Iterate through each certificate + for (const cert of certs) { + try { + // Create an X509 certificate object + const x509 = forge.pki.certificateFromPem(cert); + + + + // Extract the Common Name (CN) from the subject + let commonName = null; + const lowerKeyAlias = keyAlias.toLowerCase(); + + // In node-forge, subject is an object with attributes + if (x509.subject && x509.subject.attributes) { + for (const attr of x509.subject.attributes) { + if (attr.name === 'commonName' || attr.shortName === 'CN') { + commonName = attr.value; + break; + } + } + } + + if (commonName) { + if (commonName.toLowerCase() === lowerKeyAlias) { + return cert; + } + } + } catch (e) { + // Skip invalid certificates + continue; + } + } + + // If we get here, no matching certificate was found + ApiException.AuthException("Certificate with alias " + keyAlias + " not found in the provided PEM certificates."); + } catch (e) { + ApiException.AuthException("Error processing certificates: " + e.message); + } +}