使用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==
问题根源
签名算法填充方式不匹配
GCP密钥采用RSA-PSS填充,但验证时使用的SHA256withRSA对应PKCS#1 v1.5填充,两种填充机制不兼容,这是验证失败的核心原因。验证哈希逻辑错误
签名时手动对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
相关产品推荐
相关产品推荐

