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