From 92a54110ed424f2a80eb7ccd74e619268bedf5f7 Mon Sep 17 00:00:00 2001
From: Abhijeet <41686026+abhvsn@users.noreply.github.com>
Date: Wed, 2 Aug 2023 18:08:35 +0530
Subject: [PATCH] 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
---
app/server/appsmith-server/pom.xml | 5 +
.../server/constants/ApiConstants.java | 6 ++
.../server/helpers/SignatureVerifier.java | 95 +++++++++++++++++++
.../server/helpers/SignatureVerifierTest.java | 32 +++++++
4 files changed, 138 insertions(+)
create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/constants/ApiConstants.java
create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/SignatureVerifier.java
create mode 100644 app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/SignatureVerifierTest.java
diff --git a/app/server/appsmith-server/pom.xml b/app/server/appsmith-server/pom.xml
index 1da66de8b5..c333097e52 100644
--- a/app/server/appsmith-server/pom.xml
+++ b/app/server/appsmith-server/pom.xml
@@ -238,6 +238,11 @@
test
+
+ org.bouncycastle
+ bcprov-jdk18on
+ 1.72
+
org.junit.platform
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/ApiConstants.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/ApiConstants.java
new file mode 100644
index 0000000000..12716eb0b3
--- /dev/null
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/ApiConstants.java
@@ -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";
+}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/SignatureVerifier.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/SignatureVerifier.java
new file mode 100644
index 0000000000..b744a411b2
--- /dev/null
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/SignatureVerifier.java
@@ -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;
+ }
+}
diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/SignatureVerifierTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/SignatureVerifierTest.java
new file mode 100644
index 0000000000..cebd31a8ee
--- /dev/null
+++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/SignatureVerifierTest.java
@@ -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));
+ }
+}