Skip to content

Commit 0324441

Browse files
using serial number as response mle kid for cybersource generated p12
1 parent 8877cc5 commit 0324441

File tree

3 files changed

+247
-15
lines changed

3 files changed

+247
-15
lines changed

src/authentication/core/MerchantConfig.js

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -937,9 +937,18 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() {
937937
}
938938
}
939939

940-
// Validate KID
941-
if (typeof this.responseMleKID !== "string" || !this.responseMleKID?.trim()) {
942-
throw new ApiException.ApiException("responseMleKID is required when response MLE is enabled.", logger);
940+
// Validate KID - Auto-extract from CyberSource P12 if applicable, then validate
941+
if (!this.responseMleKID || !this.responseMleKID?.trim()) {
942+
tryAutoExtractResponseMleKid.call(this, logger);
943+
944+
if (typeof this.responseMleKID !== "string" || !this.responseMleKID?.trim()) {
945+
throw new ApiException.ApiException(
946+
"responseMleKID is required when response MLE is enabled. " +
947+
"For CyberSource-generated P12 certificates, this will be auto-extracted. " +
948+
"For other certificate types, please provide it explicitly in your configuration.",
949+
logger
950+
);
951+
}
943952
}
944953
}
945954
/**
@@ -970,6 +979,47 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() {
970979
}
971980

972981

982+
function tryAutoExtractResponseMleKid(logger) {
983+
const hasValidFilePath = typeof this.responseMlePrivateKeyFilePath === "string" && this.responseMlePrivateKeyFilePath.trim() !== "";
984+
985+
if (!hasValidFilePath) {
986+
return;
987+
}
988+
989+
const fileExtension = path.extname(this.responseMlePrivateKeyFilePath).toLowerCase();
990+
const isP12File = fileExtension === ".p12";
991+
992+
if (!isP12File) {
993+
logger.debug('Private key file is not a P12 file, skipping auto-extraction of responseMleKID');
994+
return;
995+
}
996+
997+
const isCybersourceP12 = Utility.isCybersourceP12(
998+
this.responseMlePrivateKeyFilePath,
999+
this.responseMlePrivateKeyFilePassword,
1000+
logger
1001+
);
1002+
1003+
if (!isCybersourceP12) {
1004+
logger.debug('P12 file is not a CyberSource-generated certificate, skipping auto-extraction of responseMleKID');
1005+
return;
1006+
}
1007+
1008+
logger.debug('Detected CyberSource P12 file, attempting to auto-extract responseMleKID');
1009+
try {
1010+
const extractedKid = Utility.extractResponseMleKid(
1011+
this.responseMlePrivateKeyFilePath,
1012+
this.responseMlePrivateKeyFilePassword,
1013+
this.merchantID,
1014+
logger
1015+
);
1016+
this.setResponseMleKID(extractedKid);
1017+
logger.info('Successfully auto-extracted responseMleKID from CyberSource P12 certificate');
1018+
} catch (error) {
1019+
logger.warn(`Failed to auto-extract responseMleKID from P12 file: ${error.message}. Please provide responseMleKID manually.`);
1020+
}
1021+
}
1022+
9731023
function validateAndSetMapToControlMLEonAPI(mapFromConfig) {
9741024
let tempMap;
9751025
var logger = Logger.getLogger(this, 'MerchantConfig');

src/authentication/util/Constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ 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+
CYBERSOURCE_P12_CERT_ALIAS : "CyberSource_SJC_US",
3233
MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT : "_mleCertFromMerchantConfig",
3334
MLE_CACHE_IDENTIFIER_FOR_P12_CERT : "_mleCertFromP12",
3435

src/authentication/util/Utility.js

Lines changed: 193 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,32 @@ exports.ParseMLEConfigString = function (configString, logger) {
172172
}
173173
}
174174

175+
/**
176+
* Internal helper: Parses a P12 file and returns the pkcs12 object
177+
* @param {string} filePath - Path to the P12 file
178+
* @param {string} password - Password for the P12 file
179+
* @param {object} logger - Logger object for logging messages
180+
* @returns {object} - Parsed pkcs12 object
181+
* @throws Will throw an error if file reading or parsing fails
182+
*/
183+
function parseP12File(filePath, password, logger) {
184+
logger.debug(`Parsing P12 file: ${filePath}`);
185+
186+
if (!fs.existsSync(filePath)) {
187+
logger.error(`File not found: ${filePath}`);
188+
throw new Error(Constants.FILE_NOT_FOUND + filePath);
189+
}
190+
191+
var p12Buffer = fs.readFileSync(filePath);
192+
var p12Der = forge.util.binary.raw.encode(new Uint8Array(p12Buffer));
193+
var p12Asn1 = forge.asn1.fromDer(p12Der);
194+
var p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, password);
195+
196+
logger.debug(`Successfully parsed P12 file`);
197+
198+
return p12;
199+
}
200+
175201
/**
176202
* Reads a private key from a P12 file
177203
* @param {string} filePath - Path to the P12 file
@@ -183,18 +209,7 @@ exports.readPrivateKeyFromP12 = function(filePath, password, logger) {
183209
try {
184210
logger.debug(`Reading private key from P12 file: ${filePath}`);
185211

186-
if (!fs.existsSync(filePath)) {
187-
logger.error(`File not found: ${filePath}`);
188-
ApiException.AuthException(Constants.FILE_NOT_FOUND + filePath);
189-
}
190-
191-
// Read the P12 file and convert to ASN1
192-
var p12Buffer = fs.readFileSync(filePath);
193-
var p12Der = forge.util.binary.raw.encode(new Uint8Array(p12Buffer));
194-
var p12Asn1 = forge.asn1.fromDer(p12Der);
195-
var p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, password);
196-
197-
logger.debug(`Successfully read P12 file and converted to ASN1`);
212+
var p12 = parseP12File(filePath, password, logger);
198213

199214
// Extract the private key
200215
var keyBags = p12.getBags({ bagType: forge.pki.oids.keyBag });
@@ -357,3 +372,169 @@ exports.parseAndReturnPem = function(key, logger, password, passwordPropertyName
357372
throw new Error('Unsupported key format');
358373
}
359374
}
375+
376+
/**
377+
* Checks if a P12 file is generated by CyberSource
378+
* Validates that the P12 contains a certificate with CN="CyberSource_SJC_US" and only one private key
379+
* @param {string} filePath - Path to the P12 file
380+
* @param {string} password - Password for the P12 file
381+
* @param {object} logger - Logger object for logging messages
382+
* @returns {boolean} - True if the P12 file is generated by CyberSource, false otherwise
383+
*/
384+
exports.isCybersourceP12 = function(filePath, password, logger) {
385+
try {
386+
logger.debug(`Checking if P12 file is generated by CyberSource: ${filePath}`);
387+
388+
const p12 = parseP12File(filePath, password, logger);
389+
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
390+
const certs = certBags[forge.pki.oids.certBag];
391+
392+
// Early return if no certificates found
393+
if (!certs) {
394+
logger.debug('No certificates found in P12 file');
395+
return false;
396+
}
397+
398+
logger.debug(`Found ${certs.length} certificate(s) in P12 file`);
399+
400+
// Check for CyberSource certificate using modern iteration
401+
const hasCybersourceCert = certs.some(({ cert }) => {
402+
if (!cert?.subject?.attributes) return false;
403+
404+
const cnAttr = cert.subject.attributes.find(
405+
attr => attr.name === 'commonName' || attr.shortName === 'CN'
406+
);
407+
408+
if (cnAttr) {
409+
logger.debug(`Found certificate with CN: ${cnAttr.value}`);
410+
if (cnAttr.value === Constants.CYBERSOURCE_P12_CERT_ALIAS) {
411+
logger.debug(`Found CyberSource certificate (CN=${Constants.CYBERSOURCE_P12_CERT_ALIAS})`);
412+
return true;
413+
}
414+
}
415+
return false;
416+
});
417+
418+
if (!hasCybersourceCert) {
419+
logger.debug(`P12 file does not contain CyberSource certificate (CN=${Constants.CYBERSOURCE_P12_CERT_ALIAS})`);
420+
return false;
421+
}
422+
423+
// Count private keys from both bag types
424+
const bagTypes = [
425+
{ oid: forge.pki.oids.keyBag, name: 'keyBag' },
426+
{ oid: forge.pki.oids.pkcs8ShroudedKeyBag, name: 'pkcs8ShroudedKeyBag' }
427+
];
428+
429+
let privateKeyCount = 0;
430+
for (const { oid, name } of bagTypes) {
431+
const bags = p12.getBags({ bagType: oid });
432+
const count = bags[oid]?.length || 0;
433+
if (count > 0) {
434+
privateKeyCount += count;
435+
logger.debug(`Found ${count} ${name} private key(s)`);
436+
}
437+
}
438+
439+
logger.debug(`Total private keys found: ${privateKeyCount}`);
440+
441+
// Verify exactly one private key
442+
if (privateKeyCount !== 1) {
443+
logger.debug(`P12 file does not contain exactly one private key (found ${privateKeyCount})`);
444+
return false;
445+
}
446+
447+
logger.debug('P12 file is generated by CyberSource: contains CyberSource certificate and exactly one private key');
448+
return true;
449+
450+
} catch (error) {
451+
logger.error(`Error checking if P12 file is generated by CyberSource: ${error.message}`);
452+
return false;
453+
}
454+
};
455+
456+
/**
457+
* Extracts the serial number (KID) from a certificate's subject in a P12 file where CN matches the merchantId
458+
* @param {string} filePath - Path to the P12 file
459+
* @param {string} password - Password for the P12 file
460+
* @param {string} merchantId - The merchant ID to match against the CN in the certificate subject
461+
* @param {object} logger - Logger object for logging messages
462+
* @returns {string} - The serial number extracted from the certificate's subject attributes
463+
* @throws Will throw an error if the certificate with matching CN is not found or serial number is missing
464+
*/
465+
exports.extractResponseMleKid = function(filePath, password, merchantId, logger) {
466+
try {
467+
logger.debug(`Extracting MLE KID from P12 file: ${filePath} for merchantId: ${merchantId}`);
468+
469+
const p12 = parseP12File(filePath, password, logger);
470+
471+
// Get certificate bags from P12
472+
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
473+
const certs = certBags[forge.pki.oids.certBag];
474+
475+
if (!certs || certs.length === 0) {
476+
logger.error(`No certificates found in P12 file: ${filePath}`);
477+
ApiException.AuthException(`No certificates found in P12 file: ${filePath}`);
478+
}
479+
480+
logger.debug(`Found ${certs.length} certificate(s) in P12 file`);
481+
482+
// Iterate through certificates to find one with matching CN
483+
for (let i = 0; i < certs.length; i++) {
484+
const certBag = certs[i];
485+
const cert = certBag.cert;
486+
487+
if (!cert || !cert.subject || !cert.subject.attributes) {
488+
logger.debug(`Certificate ${i + 1} has no subject attributes, skipping`);
489+
continue;
490+
}
491+
492+
// Extract CN from certificate subject
493+
let cn = null;
494+
for (const attr of cert.subject.attributes) {
495+
if (attr.name === 'commonName' || attr.shortName === 'CN') {
496+
cn = attr.value;
497+
break;
498+
}
499+
}
500+
501+
if (!cn) {
502+
logger.debug(`Certificate ${i + 1} has no CN in subject, skipping`);
503+
continue;
504+
}
505+
506+
logger.debug(`Certificate ${i + 1} CN: ${cn}`);
507+
508+
// Check if CN matches merchantId (case-insensitive)
509+
if (cn.toLowerCase() === merchantId.toLowerCase()) {
510+
logger.debug(`Found certificate with matching CN: ${cn}`);
511+
512+
// Extract serial number from certificate subject
513+
let serialNumber = null;
514+
for (const attr of cert.subject.attributes) {
515+
if (attr.name === 'serialNumber' || attr.shortName === 'serialNumber') {
516+
serialNumber = attr.value;
517+
break;
518+
}
519+
}
520+
521+
if (!serialNumber) {
522+
logger.debug(`Certificate with CN=${cn} has no serialNumber in subject, continuing search`);
523+
continue;
524+
}
525+
526+
logger.debug(`Serial number (MLE KID) extracted from certificate subject: ${serialNumber}`);
527+
528+
return serialNumber;
529+
}
530+
}
531+
532+
// If we get here, no matching certificate was found
533+
logger.error(`No certificate with CN matching merchantId (${merchantId}) and valid serialNumber found in P12 file: ${filePath}`);
534+
ApiException.AuthException(`No certificate with CN matching merchantId (${merchantId}) found in P12 file: ${filePath}`);
535+
536+
} catch (error) {
537+
logger.error(`Error extracting MLE KID from P12 file: ${filePath}: ${error.message}`);
538+
ApiException.AuthException(`Error extracting MLE KID from P12 file: ${filePath}: ${error.message}`);
539+
}
540+
};

0 commit comments

Comments
 (0)