From 39f2d2b8076a756349e267bf46e64b6218d44a34 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Tue, 25 Nov 2025 18:21:00 +0530 Subject: [PATCH 1/2] using serial number as mleKeyId in case of cybs generated p12 --- .../Core/MerchantConfiguration.php | 22 ++-- .../Jwt/JsonWebTokenGenerator.php | 5 +- lib/Authentication/Util/Cache.php | 104 +++++++++++++++++ lib/Authentication/Util/GlobalParameter.php | 1 + lib/Authentication/Util/MLEUtility.php | 86 ++++++++++++-- lib/Authentication/Util/Utility.php | 107 ++++++++++++++++++ 6 files changed, 303 insertions(+), 22 deletions(-) diff --git a/lib/Authentication/Core/MerchantConfiguration.php b/lib/Authentication/Core/MerchantConfiguration.php index 0a9d00da..4f10ed67 100644 --- a/lib/Authentication/Core/MerchantConfiguration.php +++ b/lib/Authentication/Core/MerchantConfiguration.php @@ -1734,14 +1734,6 @@ private function validateMLEConfiguration(){ throw $exception; } - // Validate responseMleKID - if (empty(trim($this->responseMleKID))) { - $error_message = "Response MLE enabled but responseMleKID is not set."; - $exception = new AuthException($error_message, 0); - self::$logger->error($error_message); - throw $exception; - } - $hasFilePath = !empty($this->responseMlePrivateKeyFilePath); $hasInMemoryKey = !empty($this->responseMlePrivateKey); @@ -1758,7 +1750,7 @@ private function validateMLEConfiguration(){ self::$logger->error($error_message); throw $exception; } - + $isP12File = false; if ($hasFilePath) { if ( !file_exists($this->responseMlePrivateKeyFilePath) || @@ -1770,6 +1762,10 @@ private function validateMLEConfiguration(){ self::$logger->error($error_message); throw $exception; } + $ext = pathinfo($this->responseMlePrivateKeyFilePath, PATHINFO_EXTENSION); + if (strcasecmp($ext, 'p12') === 0 || strcasecmp($ext, 'pfx') === 0) { + $isP12File = true; + } } else { if (!is_object($this->responseMlePrivateKey) || get_class($this->responseMlePrivateKey) !== 'OpenSSLAsymmetricKey') { $error_message = "Response MLE private key object is invalid. Expected OpenSSLAsymmetricKey"; @@ -1778,6 +1774,14 @@ private function validateMLEConfiguration(){ throw $exception; } } + + // Validate responseMleKID + if (!$isP12File && empty(trim($this->responseMleKID))) { + $error_message = "Response MLE enabled but responseMleKID is not set."; + $exception = new AuthException($error_message, 0); + self::$logger->error($error_message); + throw $exception; + } } } diff --git a/lib/Authentication/Jwt/JsonWebTokenGenerator.php b/lib/Authentication/Jwt/JsonWebTokenGenerator.php index 8e7fd5b6..234abd3f 100644 --- a/lib/Authentication/Jwt/JsonWebTokenGenerator.php +++ b/lib/Authentication/Jwt/JsonWebTokenGenerator.php @@ -9,6 +9,7 @@ use CyberSource\Authentication\Jwt\JsonWebTokenHeader as JsonWebTokenHeader; use CyberSource\Authentication\Util\GlobalParameter as GlobalParameter; use CyberSource\Logging\LogFactory as LogFactory; +use CyberSource\Authentication\Util\MLEUtility as MLEUtility; //calling the interface class JsonWebTokenGenerator implements TokenGenerator @@ -33,7 +34,7 @@ public function generateToken($resourcePath, $payloadData, $method, $merchantCon { $jwtBody = array("iat"=>$date); if (!empty($isResponseMLEForAPI)) { - $jwtBody['v-c-response-mle-kid'] = $merchantConfig->getResponseMleKID(); + $jwtBody['v-c-response-mle-kid'] = MLEUtility::validateAndAutoExtractResponseMleKid($merchantConfig); } } else if($method==GlobalParameter::POST || $method==GlobalParameter::PUT || $method==GlobalParameter::PATCH) @@ -42,7 +43,7 @@ public function generateToken($resourcePath, $payloadData, $method, $merchantCon $digest = $digestObj->generateDigest($payloadData); $jwtBody = array("digest"=>$digest,"digestAlgorithm"=>"SHA-256","iat"=>$date); if (!empty($isResponseMLEForAPI)) { - $jwtBody['v-c-response-mle-kid'] = $merchantConfig ->getResponseMleKID(); + $jwtBody['v-c-response-mle-kid'] = MLEUtility::validateAndAutoExtractResponseMleKid($merchantConfig); } } else diff --git a/lib/Authentication/Util/Cache.php b/lib/Authentication/Util/Cache.php index eef3d622..0887fed0 100644 --- a/lib/Authentication/Util/Cache.php +++ b/lib/Authentication/Util/Cache.php @@ -355,4 +355,108 @@ public function getMleResponsePrivateKeyFromFilePath($merchantConfig) return self::$file_cache[$cacheKey]['response_mle_private_key'] ?? null; } + + /** + * Get MLE KID data from cache + * + * @param MerchantConfiguration $merchantConfig The merchant configuration + * @return array|null Array with 'kid' and 'file_mod_time' keys, or null if not available + */ + public function getMLEKIdDataFromCache($merchantConfig) + { + $filePath = $merchantConfig->getResponseMlePrivateKeyFilePath(); + $cacheKey = $filePath . GlobalParameter::RESPONSE_MLE_P12_PFX_CACHE_IDENTIFIER; + + if (!isset(self::$file_cache[$cacheKey])) { + $this->setUpMLEKIdCache($merchantConfig); + } else { + $cachedMLEKId = self::$file_cache[$cacheKey]; + $currentModTime = file_exists($filePath) ? filemtime($filePath) : 0; + + if ($cachedMLEKId === null || + !isset($cachedMLEKId['file_mod_time']) || + $cachedMLEKId['file_mod_time'] !== $currentModTime) { + self::$logger->info("MLE KID cache outdated or file modified. Refreshing cache for: {$filePath}"); + $this->setUpMLEKIdCache($merchantConfig); + } + } + + return self::$file_cache[$cacheKey] ?? null; + } + + /** + * Set up MLE KID cache + * + * @param MerchantConfiguration $merchantConfig The merchant configuration + * @return void + */ + private function setUpMLEKIdCache($merchantConfig) + { + $filePath = null; + $cacheKey = null; + + try { + $filePath = $merchantConfig->getResponseMlePrivateKeyFilePath(); + $cacheKey = $filePath . GlobalParameter::RESPONSE_MLE_P12_PFX_CACHE_IDENTIFIER; + + // Get certificate from P12 file using merchant ID as alias + $merchantCert = Utility::getCertificateByAliasFromPKCS( + $filePath, + $merchantConfig->getResponseMlePrivateKeyFilePassword(), + $merchantConfig->getMerchantID() + ); + + if ($merchantCert === null) { + throw new MLEException( + "No certificate found for Response MLE Private Key file with merchant ID alias " . + $merchantConfig->getMerchantID() + ); + } + + // Check if this is a CyberSource-generated P12 file + $isCyberSourceP12 = Utility::isP12GeneratedByCyberSource( + $filePath, + $merchantConfig->getResponseMlePrivateKeyFilePassword() + ); + + $cachedMLEKId = [ + 'kid' => null, + 'file_mod_time' => file_exists($filePath) ? filemtime($filePath) : 0 + ]; + + if ($isCyberSourceP12) { + try { + $mleKID = MLEUtility::extractSerialNumber( + $merchantCert, + "Serial number not found in certificate subject field for Response MLE Private Key with merchant ID alias " . + $merchantConfig->getMerchantID() + ); + $cachedMLEKId['kid'] = $mleKID; + } catch (\Exception $e) { + throw new MLEException( + "Failed to extract serial number from certificate for Response MLE: " . $e->getMessage(), + 0, + $e + ); + } + } + + self::$file_cache[$cacheKey] = $cachedMLEKId; + + } catch (\Exception $e) { + self::$logger->error( + "Failed to load MLE KID from Response MLE Private Key file: {$filePath}. Error: " . + $e->getMessage() + ); + + // Store failure marker in cache + if ($cacheKey !== null) { + $failureMarker = [ + 'kid' => null, + 'file_mod_time' => 0 + ]; + self::$file_cache[$cacheKey] = $failureMarker; + } + } + } } diff --git a/lib/Authentication/Util/GlobalParameter.php b/lib/Authentication/Util/GlobalParameter.php index ac3391b0..89f9be62 100644 --- a/lib/Authentication/Util/GlobalParameter.php +++ b/lib/Authentication/Util/GlobalParameter.php @@ -76,6 +76,7 @@ class GlobalParameter const MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT = "_mleCertFromMerchantConfig"; const MLE_CACHE_IDENTIFIER_FOR_P12_CERT = "_mleCertFromP12"; const MLE_CACHE_IDENTIFIER_FOR_RESPONSE_PRIVATE_KEY = "_mleResponsePrivateKey"; + const RESPONSE_MLE_P12_PFX_CACHE_IDENTIFIER = "_responseMleKidFromP12"; const MERCHANT_SECRET_KEY_REQ = " MerchantSecretKey is Mandatory\n"; const KEY_PASSWORD_EMPTY = "KeyPassword Empty/Null. Assigining merchantID value\n"; diff --git a/lib/Authentication/Util/MLEUtility.php b/lib/Authentication/Util/MLEUtility.php index 3d77bd7e..e7efb6f4 100644 --- a/lib/Authentication/Util/MLEUtility.php +++ b/lib/Authentication/Util/MLEUtility.php @@ -178,7 +178,10 @@ public static function decryptMleResponsePayload($merchantConfig, $mleResponseBo private static function generateToken($cert, $requestBody) { try { - $serialNumber = self::extractSerialNumber($cert); + $serialNumber = self::extractSerialNumber( + $cert, + "Serial number not found in certificate subject field for Request MLE encryption" + ); $publicKey = openssl_pkey_get_details(openssl_pkey_get_public($cert))['key']; @@ -271,27 +274,88 @@ public static function getMLECert($merchantConfig) } } - public static function extractSerialNumber($cert) + public static function extractSerialNumber($cert, $errorMessage = null) { try { $certDetails = openssl_x509_parse($cert); - $serialNumber = null; if (isset($certDetails['subject']['serialNumber'])) { - $serialNumber = $certDetails['subject']['serialNumber']; + return $certDetails['subject']['serialNumber']; } - if ($serialNumber === null) { - self::$logger->warning("Serial number not found in MLE certificate for alias."); - $serialNumber = $certDetails['serialNumber']; - } - return $serialNumber; + $errorMsg = $errorMessage ?? "Serial number not found in certificate subject field."; + self::$logger->error($errorMsg); + throw new MLEException($errorMsg); + + } catch (MLEException $e) { + throw $e; } catch (\Exception $e) { self::$logger->error("Error extracting serial number from certificate: " . $e->getMessage()); throw new MLEException("Error extracting serial number from certificate: " . $e->getMessage()); } } - + /** + * Validates and auto-extracts the Response MLE KID from merchant configuration + * + * This function checks if the Response MLE KID is already configured. If not, + * it attempts to extract it from the Response MLE private key file (P12/PFX). + * + * @param \CyberSource\Authentication\Core\MerchantConfiguration $merchantConfig The merchant configuration + * @return string The validated or extracted Response MLE KID + * @throws MLEException If KID cannot be determined or extraction fails + */ + public static function validateAndAutoExtractResponseMleKid($merchantConfig) + { + if (self::$logger === null) { + self::$logger = (new LogFactory())->getLogger(\CyberSource\Utilities\Helpers\ClassHelper::getClassName(static::class), $merchantConfig->getLogConfiguration()); + } + + // If responseMlePrivateKey is provided directly (in-memory), use the configured responseMleKID + if ($merchantConfig->getResponseMlePrivateKey() !== null) { + self::$logger->debug("responseMlePrivateKey is provided directly, using configured responseMleKID"); + return $merchantConfig->getResponseMleKID(); + } + + // Attempt to auto-extract KID from Response MLE private key file (CyberSource P12) + $cybsKid = null; + try { + if (!isset(self::$cache)) { + self::$cache = new Cache(); + } + + $kidData = self::$cache->getMLEKIdDataFromCache($merchantConfig); + + if ($kidData !== null && isset($kidData['kid']) && !empty($kidData['kid'])) { + $cybsKid = $kidData['kid']; + self::$logger->debug("Successfully auto-extracted responseMleKID from CyberSource P12 certificate"); + } + } catch (\Exception $e) { + self::$logger->debug("Could not auto-extract responseMleKID from certificate: " . $e->getMessage()); + } + + // Get manually configured KID + $configuredKID = $merchantConfig->getResponseMleKID(); + $configuredKID = (is_string($configuredKID) && trim($configuredKID) !== '') ? trim($configuredKID) : null; + + // Validate and determine which KID to use + if ($cybsKid === null && $configuredKID === null) { + throw new MLEException( + "responseMleKID is required when response MLE is enabled. " . + "Could not auto-extract from certificate and no manual configuration provided. " . + "Please provide responseMleKID explicitly in your configuration." + ); + } else if ($cybsKid === null) { + self::$logger->debug("Using manually configured responseMleKID"); + return $configuredKID; + } else if ($configuredKID === null) { + self::$logger->debug("Using auto-extracted responseMleKID from CyberSource certificate"); + return $cybsKid; + } else if ($cybsKid !== $configuredKID) { + self::$logger->warning("Auto-extracted responseMleKID does not match manually configured responseMleKID. Using configured value as preference"); + } + + return $configuredKID; + } } -?> \ No newline at end of file +?> diff --git a/lib/Authentication/Util/Utility.php b/lib/Authentication/Util/Utility.php index 43441899..03e2663b 100644 --- a/lib/Authentication/Util/Utility.php +++ b/lib/Authentication/Util/Utility.php @@ -330,4 +330,111 @@ public static function convertKeyObjectToPem($key): string return $pemString; } + + /** + * Get certificate by alias from PKCS#12 file + * + * @param string $filePath Path to the PKCS#12 file + * @param string $password Password for the PKCS#12 file + * @param string $alias Alias/CN to search for + * @return string|null Certificate PEM string or null if not found + * @throws \Exception If file cannot be read or parsed + */ + public static function getCertificateByAliasFromPKCS(string $filePath, string $password, string $alias) + { + if (!file_exists($filePath) || !is_readable($filePath)) { + throw new \Exception("PKCS#12 file not found or not readable: {$filePath}"); + } + + $pkcs12 = file_get_contents($filePath); + if ($pkcs12 === false) { + throw new \Exception("Unable to read PKCS#12 file: {$filePath}"); + } + + $certs = []; + if (!openssl_pkcs12_read($pkcs12, $certs, $password)) { + $err = openssl_error_string(); + throw new \Exception("Unable to parse PKCS#12: {$filePath}. OpenSSL: {$err}"); + } + + // Try to find certificate by alias using existing method + try { + return self::findCertByAlias($certs, $alias); + } catch (\Exception $e) { + return null; + } + } + + /** + * Check if P12 file was generated by CyberSource + * + * @param string $filePath Path to the PKCS#12 file + * @param string $password Password for the PKCS#12 file + * @return bool True if generated by CyberSource, false otherwise + */ + public static function isP12GeneratedByCyberSource(string $filePath, string $password): bool + { + try { + if (!file_exists($filePath) || !is_readable($filePath)) { + return false; + } + + $pkcs12 = file_get_contents($filePath); + if ($pkcs12 === false) { + return false; + } + + $certs = []; + if (!openssl_pkcs12_read($pkcs12, $certs, $password)) { + return false; + } + + // Check if main certificate exists + if (!isset($certs['cert'])) { + return false; + } + + $certData = openssl_x509_parse($certs['cert']); + if ($certData === false) { + return false; + } + + // Check for CyberSource-specific indicators in the certificate + // CyberSource certificates typically have specific issuer or subject patterns + if (isset($certData['issuer'])) { + $issuer = $certData['issuer']; + + // Check for CyberSource in O (Organization) or CN (Common Name) + if (isset($issuer['O']) && stripos($issuer['O'], 'CyberSource') !== false) { + return true; + } + if (isset($issuer['CN']) && stripos($issuer['CN'], 'CyberSource') !== false) { + return true; + } + } + + if (isset($certData['subject'])) { + $subject = $certData['subject']; + + // Check for CyberSource in subject + if (isset($subject['O']) && stripos($subject['O'], 'CyberSource') !== false) { + return true; + } + } + + // Check extensions for CyberSource-specific OIDs or patterns + if (isset($certData['extensions'])) { + foreach ($certData['extensions'] as $key => $value) { + if (stripos($value, 'CyberSource') !== false) { + return true; + } + } + } + + return false; + } catch (\Exception $e) { + return false; + } + } + } From 82de2fb161437000cd43518142c4d04bef123220 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Tue, 25 Nov 2025 22:23:36 +0530 Subject: [PATCH 2/2] fixed checking of cybersource generated p12 --- lib/Authentication/Util/Utility.php | 46 +++-------------------------- 1 file changed, 4 insertions(+), 42 deletions(-) diff --git a/lib/Authentication/Util/Utility.php b/lib/Authentication/Util/Utility.php index 03e2663b..e308933d 100644 --- a/lib/Authentication/Util/Utility.php +++ b/lib/Authentication/Util/Utility.php @@ -388,50 +388,12 @@ public static function isP12GeneratedByCyberSource(string $filePath, string $pas if (!openssl_pkcs12_read($pkcs12, $certs, $password)) { return false; } - - // Check if main certificate exists - if (!isset($certs['cert'])) { - return false; - } - - $certData = openssl_x509_parse($certs['cert']); - if ($certData === false) { + try { + $cert = self::findCertByAlias($certs, GlobalParameter::DEFAULT_MLE_ALIAS_FOR_CERT); + return $cert !== null; + } catch (\Exception $e) { return false; } - - // Check for CyberSource-specific indicators in the certificate - // CyberSource certificates typically have specific issuer or subject patterns - if (isset($certData['issuer'])) { - $issuer = $certData['issuer']; - - // Check for CyberSource in O (Organization) or CN (Common Name) - if (isset($issuer['O']) && stripos($issuer['O'], 'CyberSource') !== false) { - return true; - } - if (isset($issuer['CN']) && stripos($issuer['CN'], 'CyberSource') !== false) { - return true; - } - } - - if (isset($certData['subject'])) { - $subject = $certData['subject']; - - // Check for CyberSource in subject - if (isset($subject['O']) && stripos($subject['O'], 'CyberSource') !== false) { - return true; - } - } - - // Check extensions for CyberSource-specific OIDs or patterns - if (isset($certData['extensions'])) { - foreach ($certData['extensions'] as $key => $value) { - if (stripos($value, 'CyberSource') !== false) { - return true; - } - } - } - - return false; } catch (\Exception $e) { return false; }