In my Spring boot app, I have a Filter that generates a signature for each response. I have created an Asymmetric Key in GCP(generated by GCP itself)
The key algorithm is 2048 bit RSA key PSS Padding - SHA256 Digest
Here is my class which does the signing and verification. Verification will be moved to the front end, but for now, since it is just a test I have added everything to a single class:
@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");
}
}
The issue is the result of verified
varaibale is always false for some reason. Any suggestion on what could be the issue in this code?
Example of payload:
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}]}
Example of signature in response header:
R8e+LjoD1GDVhXB8ZqzS+22Lf2SIADU3nCKHtHfJ6la5trSwAplyVmnKl9kT79yZO/bBaXUT25yyVEIicj/um71Ja5czl0DbWcF48oJNVpN1Hol0TJTQOMbKtBhN63H97Pir4u1SLEC9yF2Xkhvuf0fmM4tVcSqnfwZymNnU6TpKGnF7WGhulrW4esGsKXw2zjGIhSfSkZNv74VOy2c+FUX3tTD/oDA9QtV2mQdCPXhiFrh9h9Ukn2u2ysOzGuhmnhFqP98tzNRBjftijv2ZiSdRDm5sqsc2kREp/33DrQUmFe4ywBmDS6l6yI2oCWfTjXQDdLXTvRGh9rRU8zOynQ==
Here is the documentation I was following: Creating and validating digital signatures
It looks like you are signing with an RSASSA-PSS scheme that uses SHA-256 for at least one of the hashes (message or MGF1 PRF) probably both because it is common to make them the same, but trying to verify with an RSASSA-PKCS1-v1_5 (aka classic or traditional) scheme which is very different.
Using the 'standard' (Oracle/OpenJDK) provider(s) do Signature.getInstance("RSASSA-PSS")
then .setParameter()
with a suitable instance of PSSParameterSpec
which is probably ("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1)
. Then .update()
the data and .verify()
the signature as usual.
If you have, or can (easily enough) get, the BouncyCastle provider, and as above both hashes are the same, you can simply .getInstance(scheme,"BC")
for any of:
"SHA256withRSA/PSS"
"SHA256withRSASSA-PSS"
"SHA256withRSAandMGF1"
(in either upper or lower case, as is always true of JCA algorithm names) -- these are all aliases of the same algorithm -- and then use it as normal.