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

使用GCP生成的非对称密钥时签名验证失败问题排查

问题

Spring Boot应用中通过Filter为请求响应生成签名,使用GCP自动生成的2048 bit RSA key PSS Padding - SHA256 Digest非对称密钥。签名逻辑正常,但验证时verified变量始终为false,求排查问题。

代码实现

@Configuration
@Slf4j
public class ResponseFilterConfig {
    GcpKmsConfiguration gcpKmsConfiguration;
    private CryptoKeyVersionName asymmetricSignKeyForAPI;
    @Autowired
    KeyManagementServiceClient keyManagementServiceClient;
    @Autowired
    GcpKmsProperties gcpKmsProperties;
    @Autowired
    GCPKeyUseCase gcpKeyUseCase;

    public ResponseFilterConfig(GcpKmsConfiguration gcpKmsConfiguration) {
        this.gcpKmsConfiguration = gcpKmsConfiguration;
        this.asymmetricSignKeyForAPI = gcpKmsConfiguration.asymmetricSignKeyForAPI();
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    @ConditionalOnWebApplication
    public Filter responseLoggingFilter() {
        return (request, response, chain) -> {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            boolean isFilterAllowed = shouldNotFilter(httpRequest);
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            ContentCachingResponseWrapper responseCacheWrapperObject = new ContentCachingResponseWrapper(httpResponse);
            chain.doFilter(request, responseCacheWrapperObject);

            byte[] responseArray = responseCacheWrapperObject.getContentAsByteArray();
            String responseStr = new String(responseArray, responseCacheWrapperObject.getCharacterEncoding());

            var dataToSign = httpResponse.getStatus() + responseStr;
            var signedData = signPayloadWithGcp(dataToSign);
            if (!isFilterAllowed) {
                httpResponse.addHeader("digital-signature", signedData);
            }
            responseCacheWrapperObject.copyBodyToResponse();
        };
    }

    private String signPayloadWithGcp(String payload) throws IOException {
        byte[] plaintext = payload.getBytes(StandardCharsets.UTF_8);
        MessageDigest sha256;
        AsymmetricSignResponse result;
        try {
            sha256 = MessageDigest.getInstance("SHA-256");
            byte[] hash = sha256.digest(plaintext);
            Digest digest = Digest.newBuilder().setSha256(ByteString.copyFrom(hash)).build();
            result = keyManagementServiceClient.asymmetricSign(asymmetricSignKeyForAPI, digest);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Error signing payload with GCP", e);
        }

        String s = Base64.getEncoder().encodeToString(result.getSignature().toByteArray());

        //for test verify the signature
        try {
            var res = verifySignature(s, payload);
            log.info("Signature verification result: {}", res);
        } catch (Exception e) {
            log.error("Exception", e);
        }
        return s;
    }

    private boolean verifySignature(String signedData, String payload) throws Exception {
        byte[] signatureBytes = Base64.getDecoder().decode(signedData);

        byte[] plaintext = payload.getBytes(StandardCharsets.UTF_8);

        // Get the public key.
        com.google.cloud.kms.v1.PublicKey publicKey = fetchPublicKey();

        // Convert the public PEM key to a DER key (see helper below).
        byte[] derKey = convertPemToDer(publicKey.getPem());
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(derKey);
        java.security.PublicKey rsaKey = KeyFactory.getInstance("RSA").generatePublic(keySpec);

        // Verify the 'RSA_SIGN_PKCS1_2048_SHA256' signature.
        // For other key algorithms:
        // http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Signature
        Signature rsaVerify = Signature.getInstance("SHA256withRSA");
        rsaVerify.initVerify(rsaKey);
        rsaVerify.update(plaintext);

        // Verify the signature.
        boolean verified = rsaVerify.verify(signatureBytes);
        System.out.printf("Signature verified: %s", verified);
        return verified;
    }

    private byte[] convertPemToDer(String pem) {
        BufferedReader bufferedReader = new BufferedReader(new StringReader(pem));
        String encoded =
                bufferedReader
                        .lines()
                        .filter(line -> !line.startsWith("-----BEGIN") && !line.startsWith("-----END"))
                        .collect(Collectors.joining());
        return Base64.getDecoder().decode(encoded);
    }

    // Method to fetch public key from Google Cloud KMS
    private com.google.cloud.kms.v1.PublicKey fetchPublicKey() throws IOException {
        return gcpKeyUseCase.getPublicKey2(gcpKmsProperties.getProjectId(), gcpKmsProperties.getLocationId(), gcpKmsProperties.getSigningKeyRing(), gcpKmsProperties.getApiSignKey(), gcpKmsProperties.getApiSignKeyVersion());
    }

    protected boolean shouldNotFilter(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return requestURI.contains("actuator") || requestURI.contains("open-api") || requestURI.contains("swagger") || requestURI.contains("/api/sign-key");
    }

}

示例数据

  • 待签名载荷:
200{"cards":[{"id":"69","referenceId":"89000901","number":"4459XXXXXXXX0901","status":"OPEN","expiryDate":"0528","modifiedAt":"2024-03-27T10:31:23","customerId":"36","nameOnCard":"Bora Duran","isPinCreated":false}]}
  • 响应头签名:
R8e+LjoD1GDVhXB8ZqzS+22Lf2SIADU3nCKHtHfJ6la5trSwAplyVmnKl9kT79yZO/bBaXUT25yyVEIicj/um71Ja5czl0DbWcF48oJNVpN1Hol0TJTQOMbKtBhN63H97Pir4u1SLEC9yF2Xkhvuf0fmM4tVcSqnfwZymNnU6TpKGnF7WGhulrW4esGsKXw2zjGIhSfSkZNv74VOy2c+FUX3tTD/oDA9QtV2mQdCPXhiFrh9h9Ukn2u2ysOzGuhmnhFqP98tzNRBjftijv2ZiSdRDm5sqsc2kREp/33DrQUmFe4ywBmDS6l6yI2oCWfTjXQDdLXTvRGh9rRU8zOynQ==

问题根源

  1. 签名算法填充方式不匹配
    GCP密钥采用RSA-PSS填充,但验证时使用的SHA256withRSA对应PKCS#1 v1.5填充,两种填充机制不兼容,这是验证失败的核心原因。

  2. 验证哈希逻辑错误
    签名时手动对payload做SHA256哈希,再将哈希值传给GCP KMS签名;但验证时直接用原始payload调用Signature.update(),而SHA256withRSA会再次对payload做哈希,相当于验证“payload哈希的哈希”的签名,与实际签名数据完全不匹配。


修复方案

调整验证逻辑匹配现有签名流程

保持签名逻辑不变,修改验证方法如下:

private boolean verifySignature(String signedData, String payload) throws Exception {
    byte[] signatureBytes = Base64.getDecoder().decode(signedData);

    // 1. 手动计算payload的SHA256哈希,与签名步骤一致
    byte[] plaintext = payload.getBytes(StandardCharsets.UTF_8);
    MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
    byte[] hash = sha256.digest(plaintext);

    // 2. 获取并转换公钥
    com.google.cloud.kms.v1.PublicKey publicKey = fetchPublicKey();
    byte[] derKey = convertPemToDer(publicKey.getPem());
    X509EncodedKeySpec keySpec = new X509EncodedKeySpec(derKey);
    java.security.PublicKey rsaKey = KeyFactory.getInstance("RSA").generatePublic(keySpec);

    // 3. 使用RSA-PSS算法验证,传入手动计算的哈希值
    Signature rsaVerify = Signature.getInstance("NONEwithRSA/PSS");
    // 配置PSS参数匹配GCP KMS默认设置
    PSSParameterSpec pssSpec = new PSSParameterSpec(
        "SHA-256",
        "MGF1",
        MGF1ParameterSpec.SHA256,
        32, // SHA256哈希长度为32字节
        1
    );
    rsaVerify.setParameter(pssSpec);
    rsaVerify.initVerify(rsaKey);
    rsaVerify.update(hash);

    boolean verified = rsaVerify.verify(signatureBytes);
    System.out.printf("Signature verified: %s", verified);
    return verified;
}

额外检查项

  • 确认fetchPublicKey方法获取的公钥与签名使用的密钥版本完全一致,避免拿错密钥导致验证失败。
  • 确保签名和验证时使用的字符编码统一(当前代码已用StandardCharsets.UTF_8,无需修改)。

内容的提问来源于stack exchange,提问作者Yuri

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 20:50:57