feat: Add a method to verify signature for cloud services response (#24766)
## Description As of now the CS API does not have signature verification which can lead to data tampering for CS API response. This PR adds the method to add signature verification for CS API responses. Corresponding PRs: CS: https://github.com/appsmithorg/cloud-services/pull/1023 #### PR fixes following issue(s) Fixes https://github.com/appsmithorg/cloud-services/issues/1037 #### Type of change - New feature (non-breaking change which adds functionality) ## Testing #### How Has This Been Tested? - [ ] Manual ## Checklist: #### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
This commit is contained in:
parent
0521ba2c0d
commit
92a54110ed
|
|
@ -238,6 +238,11 @@
|
|||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
<version>1.72</version>
|
||||
</dependency>
|
||||
<!-- Only required to run junit5 test from IDE -->
|
||||
<dependency>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.appsmith.server.constants;
|
||||
|
||||
public class ApiConstants {
|
||||
public static final String DATE = "Date";
|
||||
public static final String CLOUD_SERVICES_SIGNATURE = "X-Appsmith-CS-Signature";
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.appsmith.server.helpers;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
|
||||
import org.bouncycastle.crypto.signers.Ed25519Signer;
|
||||
import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil;
|
||||
import org.pf4j.util.StringUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Base64;
|
||||
|
||||
import static com.appsmith.server.constants.ApiConstants.CLOUD_SERVICES_SIGNATURE;
|
||||
import static com.appsmith.server.constants.ApiConstants.DATE;
|
||||
|
||||
@Slf4j
|
||||
public class SignatureVerifier {
|
||||
|
||||
// Public key for verifying signatures with ED25519 scheme.
|
||||
private static final String PUBLIC_VERIFICATION_KEY =
|
||||
"AAAAC3NzaC1lZDI1NTE5AAAAICNwJ+zx2opXjjOga/YyzRxb2czvNgQ/twA+miCKDIX3";
|
||||
|
||||
private static final String TIMESTAMP = "timestamp";
|
||||
|
||||
private static final String EQUAL = "=";
|
||||
|
||||
private static final AsymmetricKeyParameter publicKeyParameters;
|
||||
|
||||
static {
|
||||
publicKeyParameters =
|
||||
OpenSSHPublicKeyUtil.parsePublicKey(Base64.getDecoder().decode(PUBLIC_VERIFICATION_KEY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to verify the API signature from CS.
|
||||
* @param headers Response headers from CS
|
||||
* @return If the signature is valid
|
||||
*/
|
||||
public static boolean isSignatureValid(HttpHeaders headers) {
|
||||
if (CollectionUtils.isEmpty(headers.get(CLOUD_SERVICES_SIGNATURE))
|
||||
|| CollectionUtils.isEmpty(headers.get(DATE))) {
|
||||
return false;
|
||||
}
|
||||
String signature = headers.get(CLOUD_SERVICES_SIGNATURE).get(0);
|
||||
String date = headers.get(DATE).get(0);
|
||||
if (StringUtils.isNullOrEmpty(signature) || StringUtils.isNullOrEmpty(date)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (publicKeyParameters == null) {
|
||||
return false;
|
||||
}
|
||||
return isSignatureValid(signature, date);
|
||||
} catch (Exception exception) {
|
||||
log.debug("Error occurred while verifying CS signature.", exception);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSignatureValid(String signature, String dateHeader) {
|
||||
String[] signatureParts = signature.split("\\.", 2);
|
||||
if (signatureParts.length != 2) {
|
||||
return false;
|
||||
}
|
||||
String signingData = signatureParts[0];
|
||||
String encodedSignature = signatureParts[1];
|
||||
|
||||
// Decode base64 signature and signing data to byte arrays
|
||||
byte[] signatureBytes = Base64.getUrlDecoder().decode(encodedSignature);
|
||||
byte[] signingDataBytes = signingData.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// Set up Ed25519 verifier
|
||||
Ed25519Signer verifier = new Ed25519Signer();
|
||||
verifier.init(false, publicKeyParameters);
|
||||
verifier.update(signingDataBytes, 0, signingDataBytes.length);
|
||||
|
||||
// Verify the signature to check if the data is tampered
|
||||
if (verifier.verifySignature(signatureBytes)) {
|
||||
String decodedData = new String(Base64.getDecoder().decode(signingData));
|
||||
// To avoid the replay attacks check if the date provided in the header is within 24hrs and matches with
|
||||
// the date used while encoding the data.
|
||||
// Data format for ref: String.format("data=%s_timestamp=%s", dataset.toString(), date)
|
||||
String timestampFieldFormat = TIMESTAMP + EQUAL;
|
||||
String date =
|
||||
decodedData.substring(decodedData.indexOf(timestampFieldFormat) + timestampFieldFormat.length());
|
||||
|
||||
return dateHeader.equals(date)
|
||||
&& Instant.parse(date).isAfter(Instant.now().minus(24, ChronoUnit.HOURS));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.appsmith.server.helpers;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.appsmith.server.constants.ApiConstants.CLOUD_SERVICES_SIGNATURE;
|
||||
import static com.appsmith.server.constants.ApiConstants.DATE;
|
||||
|
||||
class SignatureVerifierTest {
|
||||
|
||||
@Test
|
||||
public void invalidSignature_verifySignatureFormat_returnFalse() {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set(DATE, Instant.now().toString());
|
||||
headers.set(CLOUD_SERVICES_SIGNATURE, "");
|
||||
Assertions.assertFalse(SignatureVerifier.isSignatureValid(headers));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidSignature_verifySignature_returnFalse() {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set(DATE, Instant.now().toString());
|
||||
headers.set(CLOUD_SERVICES_SIGNATURE, UUID.randomUUID().toString());
|
||||
Assertions.assertFalse(SignatureVerifier.isSignatureValid(headers));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user