From dfd044582116ce2a01b8ab844321d9c7885063f5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 9 Oct 2026 14:50:50 +0200 Subject: [PATCH] use openssl for certificate verification Signed-off-by: alperozturk96 --- .../utils/CertificateValidatorTests.kt | 46 +++++++++- app/src/main/cpp/CMakeLists.txt | 12 +++ app/src/main/cpp/certificate_validator.cpp | 91 +++++++++++++++++++ .../setupEncryption/CertificateValidator.kt | 41 ++------- .../android/utils/crypto/CryptoStringUtils.kt | 5 - 5 files changed, 151 insertions(+), 44 deletions(-) create mode 100644 app/src/main/cpp/certificate_validator.cpp diff --git a/app/src/androidTest/java/com/nextcloud/utils/CertificateValidatorTests.kt b/app/src/androidTest/java/com/nextcloud/utils/CertificateValidatorTests.kt index 127858586a22..7c1adcac67bc 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/CertificateValidatorTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/CertificateValidatorTests.kt @@ -7,14 +7,18 @@ package com.nextcloud.utils +import android.util.Base64 import androidx.test.platform.app.InstrumentationRegistry import com.google.gson.Gson import com.owncloud.android.datamodel.Credentials import com.owncloud.android.ui.dialog.setupEncryption.CertificateValidator import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import java.io.InputStreamReader +import java.security.KeyPairGenerator class CertificateValidatorTests { @@ -30,16 +34,48 @@ class CertificateValidatorTests { sut = null } - @Test - fun testValidateWhenGivenValidServerKeyAndCertificateShouldReturnTrue() { + private fun readCredentials(): Credentials { val inputStream = InstrumentationRegistry.getInstrumentation().context.assets.open("credentials.json") - val credentials = InputStreamReader(inputStream).use { reader -> + return InputStreamReader(inputStream).use { reader -> Gson().fromJson(reader, Credentials::class.java) } + } + + private fun generateUnrelatedPublicKeyPem(): String { + val publicKey = KeyPairGenerator.getInstance("RSA") + .apply { initialize(RSA_KEY_SIZE) } + .generateKeyPair() + .public + val base64 = Base64.encodeToString(publicKey.encoded, Base64.DEFAULT) + return "-----BEGIN PUBLIC KEY-----\n$base64-----END PUBLIC KEY-----\n" + } + + @Test + fun testValidateWhenGivenValidServerKeyAndCertificateShouldReturnTrue() { + val credentials = readCredentials() + + assertTrue(sut?.validate(credentials.publicKey, credentials.certificate) ?: false) + } + + @Test + fun testValidateWhenCertificateIsNotSignedByServerKeyShouldReturnFalse() { + val credentials = readCredentials() + + assertFalse(sut?.validate(generateUnrelatedPublicKeyPem(), credentials.certificate) ?: true) + } + + @Test + fun testValidateWhenGivenMalformedInputShouldReturnFalse() { + val credentials = readCredentials() + + assertFalse(sut?.validate("", credentials.certificate) ?: true) + assertFalse(sut?.validate(credentials.publicKey, "") ?: true) + assertFalse(sut?.validate(credentials.publicKey, credentials.publicKey) ?: true) + } - val isCertificateValid = sut?.validate(credentials.publicKey, credentials.certificate) ?: false - assert(isCertificateValid) + companion object { + private const val RSA_KEY_SIZE = 2048 } } diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 7a7a10b2eabc..c3f736b01db8 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -21,4 +21,16 @@ target_link_libraries( openssl::crypto android log +) + +add_library( + certificate_validator + SHARED + certificate_validator.cpp +) + +target_link_libraries( + certificate_validator + openssl::crypto + log ) \ No newline at end of file diff --git a/app/src/main/cpp/certificate_validator.cpp b/app/src/main/cpp/certificate_validator.cpp new file mode 100644 index 000000000000..b59078ab1954 --- /dev/null +++ b/app/src/main/cpp/certificate_validator.cpp @@ -0,0 +1,91 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include +#include +#include +#include +#include +#include +#include + +#define LOG_TAG "CertificateValidator" +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) + +static constexpr size_t OPENSSL_ERROR_BUFFER_SIZE = 256; + +static void logOpenSslError(const char* message) { + char reason[OPENSSL_ERROR_BUFFER_SIZE]; + ERR_error_string_n(ERR_get_error(), reason, sizeof(reason)); + LOGE("%s: %s", message, reason); + ERR_clear_error(); +} + +static EVP_PKEY* readPublicKey(JNIEnv* env, jstring publicKeyPem) { + const char* chars = env->GetStringUTFChars(publicKeyPem, nullptr); + BIO* bio = BIO_new_mem_buf(chars, -1); + EVP_PKEY* publicKey = bio == nullptr ? nullptr : PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr); + BIO_free(bio); + env->ReleaseStringUTFChars(publicKeyPem, chars); + return publicKey; +} + +static X509* readCertificate(JNIEnv* env, jstring certificatePem) { + const char* chars = env->GetStringUTFChars(certificatePem, nullptr); + BIO* bio = BIO_new_mem_buf(chars, -1); + X509* certificate = bio == nullptr ? nullptr : PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); + BIO_free(bio); + env->ReleaseStringUTFChars(certificatePem, chars); + return certificate; +} + +// X509_cmp_current_time returns -1 when the time is in the past, 1 when it is in the future and 0 on error. +static bool isWithinValidityPeriod(const X509* certificate) { + return X509_cmp_current_time(X509_get0_notBefore(certificate)) < 0 && + X509_cmp_current_time(X509_get0_notAfter(certificate)) > 0; +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_owncloud_android_ui_dialog_setupEncryption_CertificateValidator_verifyCertificate( + JNIEnv* env, + jobject /* thiz */, + jstring serverPublicKeyPem, + jstring certificatePem +) { + if (serverPublicKeyPem == nullptr || certificatePem == nullptr) { + LOGE("Server public key or certificate is missing"); + return JNI_FALSE; + } + + EVP_PKEY* serverPublicKey = readPublicKey(env, serverPublicKeyPem); + if (serverPublicKey == nullptr) { + logOpenSslError("Failed to parse server public key"); + return JNI_FALSE; + } + + X509* certificate = readCertificate(env, certificatePem); + if (certificate == nullptr) { + logOpenSslError("Failed to parse certificate"); + EVP_PKEY_free(serverPublicKey); + return JNI_FALSE; + } + + jboolean isValid = JNI_FALSE; + if (!isWithinValidityPeriod(certificate)) { + LOGE("Certificate is expired or not yet valid"); + } else if (X509_verify(certificate, serverPublicKey) != 1) { + logOpenSslError("Certificate is not signed by the server public key"); + } else { + LOGD("Client certificate is valid against server public key"); + isValid = JNI_TRUE; + } + + X509_free(certificate); + EVP_PKEY_free(serverPublicKey); + return isValid; +} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/setupEncryption/CertificateValidator.kt b/app/src/main/java/com/owncloud/android/ui/dialog/setupEncryption/CertificateValidator.kt index 60acc2cd10ef..6e0d9f33a68a 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/setupEncryption/CertificateValidator.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/setupEncryption/CertificateValidator.kt @@ -7,51 +7,24 @@ package com.owncloud.android.ui.dialog.setupEncryption -import android.util.Base64 -import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.utils.crypto.CryptoStringUtils -import java.io.ByteArrayInputStream -import java.security.KeyFactory -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.security.spec.X509EncodedKeySpec import javax.inject.Inject -@Suppress("EmptyClassBlock") class CertificateValidator @Inject constructor() { - private val tag = "CertificateValidator" /** * Validates certificate with given public key * - * @param serverPublicKeyString Public key with header + * @param serverPublicKeyString Public key in PEM format * @param certificate Certificate in PEM format */ - @Suppress("TooGenericExceptionCaught") - fun validate(serverPublicKeyString: String, certificate: String): Boolean { - val contentOfServerKey = CryptoStringUtils.rawPublicKey(serverPublicKeyString) + fun validate(serverPublicKeyString: String, certificate: String): Boolean = + verifyCertificate(serverPublicKeyString, certificate) - return try { - val decodedPublicKey = Base64.decode(contentOfServerKey, Base64.NO_WRAP) + private external fun verifyCertificate(serverPublicKeyPem: String, certificatePem: String): Boolean - val keySpec = X509EncodedKeySpec(decodedPublicKey) - val keyFactory = KeyFactory.getInstance("RSA") - val serverPublicKey = keyFactory.generatePublic(keySpec) - - val certificateFactory = CertificateFactory.getInstance("X.509") - val certificateInputStream = ByteArrayInputStream(certificate.toByteArray()) - val x509Certificate = certificateFactory.generateCertificate(certificateInputStream) as X509Certificate - - // Check date of the certificate - x509Certificate.checkValidity() - - // Verify certificate with serverPublicKey - x509Certificate.verify(serverPublicKey) - Log_OC.d(tag, "Client certificate is valid against server public key") - true - } catch (e: Exception) { - Log_OC.d(tag, "Client certificate is not valid against the server public key: $e") - false + companion object { + init { + System.loadLibrary("certificate_validator") } } } diff --git a/app/src/main/java/com/owncloud/android/utils/crypto/CryptoStringUtils.kt b/app/src/main/java/com/owncloud/android/utils/crypto/CryptoStringUtils.kt index 3a6a01e4f11d..778092692a7c 100644 --- a/app/src/main/java/com/owncloud/android/utils/crypto/CryptoStringUtils.kt +++ b/app/src/main/java/com/owncloud/android/utils/crypto/CryptoStringUtils.kt @@ -8,11 +8,6 @@ package com.owncloud.android.utils.crypto object CryptoStringUtils { - fun rawPublicKey(publicKey: String): String = publicKey - .replace("-----BEGIN PUBLIC KEY-----", "") - .replace("-----END PUBLIC KEY-----", "") - .replace("\\s+".toRegex(), "") - fun rawPrivateKey(privateKey: String): String = privateKey .replace("\n".toRegex(), "") .replace("-----BEGIN PRIVATE KEY-----", "")