You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 22:09:51