Cybersource Microform V0.4升级至V2:Context密钥公钥验证失败求助
Cybersource Microform V2 捕获上下文令牌签名验证失败排查
将Cybersource Microform从V0.4版本升级到V2版本后,使用自行编写的Java类进行Context密钥与公钥的验证时,验证始终失败,在代码行boolean isValid = signature.verify(signatureBytes);处一直返回false。要求仅使用Java原生库完成验证或转换操作,需排查代码问题并给出建议。
问题代码
package com.wwg.integrations.validator; import de.hybris.platform.servicelayer.config.ConfigurationService; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.HttpClients; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import java.nio.charset.StandardCharsets; import javax.net.ssl.SSLContext; import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; import java.security.KeyFactory; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.Signature; import java.security.cert.X509Certificate; import java.security.spec.RSAPublicKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.Base64; public class CaptureContextTokenValidator { private static final Logger LOG = LoggerFactory.getLogger(WWGCaptureContextTokenValidator.class); public static final String CYBERSOURCE_PUBLIC_KEY_URL = "cybersource.public.key.url"; private ConfigurationService configurationService; public boolean validateCaptureContextToken(String captureContextToken) { if (captureContextToken == null || captureContextToken.isEmpty()) { LOG.error("Capture context token is null or empty"); return false; } String kidId = extractKidFromJWTToken(captureContextToken); JSONObject jwkJson = fetchRSAKeyUsingKidId(kidId); try { PublicKey publicKey = extractPublicKeyFromJWK(jwkJson); LOG.info("Public Key: " + publicKey); return validateCaptureContextWithPublicKey(captureContextToken, publicKey); } catch (Exception e) { throw new RuntimeException("Error during validation", e); } } private JSONObject fetchRSAKeyUsingKidId(String kidId) { RestTemplate restTemplate = getRestTemplate(); final String publicKeyURL = getConfigurationService().getConfiguration().getString(CYBERSOURCE_PUBLIC_KEY_URL) + kidId; LOG.info("Fetching public key using : " + publicKeyURL); try { URI uri = new URI(publicKeyURL); HttpEntity<String> requestEntity = new HttpEntity<>(null, null); ResponseEntity<String> result = restTemplate.exchange(uri, HttpMethod.GET, requestEntity, String.class); String jsonResponse = result.getBody(); LOG.info("Response from public key fetch: " + jsonResponse); return new JSONObject(jsonResponse); } catch (URISyntaxException e) { throw new RuntimeException("Invalid URI", e); } catch (Exception e) { throw new RuntimeException("Error parsing JSON response", e); } } private RestTemplate getRestTemplate() { try { SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom() .loadTrustMaterial(null, (X509Certificate[] chain, String authType) -> true) .build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(HttpClients.custom().setSSLSocketFactory(new SSLConnectionSocketFactory(sslContext)).build()); return new RestTemplate(requestFactory); } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) { throw new RuntimeException("Error creating RestTemplate", e); } } public PublicKey extractPublicKeyFromJWK(JSONObject jwk) throws Exception { // Extract the modulus and exponent String n = jwk.getString("n"); String e = jwk.getString("e"); // Add padding to make Base64 decoding safe n = addBase64Padding(n); e = addBase64Padding(e); // Decode using URL-safe decoder byte[] modulusBytes = Base64.getUrlDecoder().decode(n); byte[] exponentBytes = Base64.getUrlDecoder().decode(e); // Convert to BigInteger BigInteger modulus = new BigInteger(1, modulusBytes); BigInteger exponent = new BigInteger(1, exponentBytes); // Create the public key spec RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePublic(spec); } private String addBase64Padding(String base64) { int paddingLength = (4 - base64.length() % 4) % 4; return base64 + "=".repeat(paddingLength); } private boolean validateCaptureContextWithPublicKey(String captureContextToken, PublicKey publicKey) { try { // Split the JWT token into parts String[] parts = captureContextToken.split("\\."); if (parts.length != 3) { LOG.error("Invalid JWT token format: {}", captureContextToken); return false; } // Construct header and payload String headerAndPayload = parts[0] + "." + parts[1]; byte[] signatureBytes = Base64.getUrlDecoder().decode(parts[2]); // Initialize the Signature instance Signature signature = Signature.getInstance("SHA256withRSA"); signature.initVerify(publicKey); // Update the Signature instance with header and payload signature.update(headerAndPayload.getBytes(StandardCharsets.US_ASCII)); // Verify the signature boolean isValid = signature.verify(signatureBytes); LOG.info("Signature valid? {}", isValid); return isValid; } catch (Exception e) { LOG.error("Error validating JWT signature", e); return false; } } private String extractKidFromJWTToken(String jwt) { try { String[] parts = jwt.split("\\."); if (parts.length < 2) { throw new IllegalArgumentException("Invalid JWT token format"); } String bodyData = new String(Base64.getUrlDecoder().decode(parts[1])); JSONObject jsonObject = new JSONObject(bodyData); return jsonObject .getJSONObject("flx") .getJSONObject("jwk") .optString("kid"); } catch (Exception e) { throw new RuntimeException("Failed to extract kid from JWT token", e); } } public ConfigurationService getConfigurationService() { return configurationService; } public void setConfigurationService(ConfigurationService configurationService) { this.configurationService = configurationService; } }
排查与修复建议
1. 修正KID提取位置
Cybersource V2的捕获上下文令牌是标准JWT,KID(密钥ID)存放在JWT的Header部分,而非Payload的flx.jwk节点。当前代码提取KID的位置错误,导致获取到错误的密钥ID,进而拿到不匹配的公钥,验证必然失败。修改extractKidFromJWTToken方法:
private String extractKidFromJWTToken(String jwt) { try { String[] parts = jwt.split("\\."); if (parts.length < 2) { throw new IllegalArgumentException("Invalid JWT token format"); } // 解码JWT的Header部分获取KID String headerData = new String(Base64.getUrlDecoder().decode(parts[0])); JSONObject headerJson = new JSONObject(headerData); return headerJson.optString("kid"); } catch (Exception e) { throw new RuntimeException("Failed to extract kid from JWT token", e); } }
2. 移除手动Base64填充逻辑
Java 8及以上版本的Base64.getUrlDecoder()支持自动处理无填充的Base64URL编码,无需手动补填充。移除addBase64Padding方法,并简化公钥提取代码:
public PublicKey extractPublicKeyFromJWK(JSONObject jwk) throws Exception { String n = jwk.getString("n"); String e = jwk.getString("e"); // 直接解码,无需手动补填充 byte[] modulusBytes = Base64.getUrlDecoder().decode(n); byte[] exponentBytes = Base64.getUrlDecoder().decode(e); BigInteger modulus = new BigInteger(1, modulusBytes); BigInteger exponent = new BigInteger(1, exponentBytes); RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePublic(spec); }
3. 严格匹配签名算法
从JWT Header的alg字段获取实际使用的签名算法,避免硬编码导致的不匹配:
private boolean validateCaptureContextWithPublicKey(String captureContextToken, PublicKey publicKey) { try { String[] parts = captureContextToken.split("\\."); if (parts.length != 3) { LOG.error("Invalid JWT token format: {}", captureContextToken); return false; } // 从Header获取签名算法 String headerData = new String(Base64.getUrlDecoder().decode(parts[0])); JSONObject headerJson = new JSONObject(headerData); String alg = headerJson.optString("alg", "SHA256withRSA"); String headerAndPayload = parts[0] + "." + parts[1]; byte[] signatureBytes = Base64.getUrlDecoder().decode(parts[2]); Signature signature = Signature.getInstance(alg); signature.initVerify(publicKey); // 改用UTF-8编码,符合JWT规范 signature.update(headerAndPayload.getBytes(StandardCharsets.UTF_8)); boolean isValid = signature.verify(signatureBytes); LOG.info("Signature valid? {}", isValid); return isValid; } catch (Exception e) { LOG.error("Error validating JWT signature", e); return false; } }
4. 验证公钥正确性
在extractPublicKeyFromJWK方法中添加日志,打印公钥的模数和十六进制值,与Cybersource后台的公钥信息对比,确认公钥生成正确:
LOG.info("公钥模数(十六进制): {}", modulus.toString(16)); LOG.info("公钥指数(十六进制): {}", exponent.toString(16));
内容的提问来源于stack exchange,提问作者Karthik
相关产品推荐
相关产品推荐

