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)); + } +}