Using Android mobile app with SSL client certificate

Checklist
I have read intro post: About the Installation Issues category
I have read the tutorials, help and searched for similar issues
I provide relevant information about my server (component names and versions, etc.)
[/] I provide a copy of my logs and healthcheck
I describe the steps I have taken to trouble shoot the problem
I describe the steps on how to reproduce the issue

Hey, i am unable to set up my android passbolt app due to the app not using my android device’s certificate store.

I have set up a passbolt server for evaluating passbolt using the official helm chart (version 0.3.1).

Chart.lock
dependencies:
- name: passbolt
  repository: https://download.passbolt.com/charts/passbolt
  version: 0.3.1
digest: sha256:5d24e4fb2118a075f905ed80e46f5b9d47a73286fdd80c74b2e15a00e631bf97
generated: "2023-05-11T15:07:56.024965819+02:00"

It runs behind an Nginx ingress that is served only through cloudflare tunnels.
I have set up a SSL client certificate that is required by cloudflare to visit my domain.
I have set up that client certificate on my phone and can visit and use the passbolt web interface just fine through web browsers which are using the device’s certificate store.
Passbolt also works from other devices with browsers making use of the SSL client certificate.

However when scanning the initial QR code to set up the app, i get a HTTP error 403.
Presumably due to the app not using the client certificate the request gets denied at cloudflare level already.

Is there currently any way to make the app use the client certificate or does this need to still be implemented?

Android app logs
Device: Google Pixel 7 Pro
Android 13 (33)
Passbolt 1.13.2-20

1:29:01 PM --> PUT https://passbolt.karpfen.dev/mobile/transfers/b0756286-a850-4e24-ad52-beef6bafa42b/56b7bf7d-9af8-4bb3-ad47-163d8112c73f.json h2 (41-byte body)
1:29:01 PM <-- 403 https://passbolt.karpfen.dev/mobile/transfers/b0756286-a850-4e24-ad52-beef6bafa42b/56b7bf7d-9af8-4bb3-ad47-163d8112c73f.json (34ms, unknown-length body)
1:29:01 PM retrofit2.HttpException: HTTP 403 
	at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:53)
	at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
	at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
	at java.lang.Thread.run(Thread.java:1012)

retrofit2.HttpException: HTTP 403 
	at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:53)
	at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
	at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
	at java.lang.Thread.run(Thread.java:1012)
1:29:01 PM Uncaught exception in thread: main
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:225)
	at com.google.gson.Gson.fromJson(Gson.java:991)
	at com.google.gson.Gson.fromJson(Gson.java:929)
	at com.passbolt.mobile.android.core.networking.ErrorHeaderMapper.getBaseResponse(ErrorHeaderMapper.kt:40)
	at com.passbolt.mobile.android.core.networking.ResponseHandler.parseErrorResponseBody(ResponseHandler.kt:79)
	at com.passbolt.mobile.android.core.networking.ResponseHandler.handleException(ResponseHandler.kt:46)
	at com.passbolt.mobile.android.passboltapi.registration.MobileTransferRepository.turnPage(MobileTransferRepository.kt:94)
	at com.passbolt.mobile.android.passboltapi.registration.MobileTransferRepository$turnPage$1.invokeSuspend(Unknown Source:16)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
	at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:42)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
	Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@30507a4, Dispatchers.Main]
Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $
	at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:385)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:214)
	... 15 more

com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:225)
	at com.google.gson.Gson.fromJson(Gson.java:991)
	at com.google.gson.Gson.fromJson(Gson.java:929)
	at com.passbolt.mobile.android.core.networking.ErrorHeaderMapper.getBaseResponse(ErrorHeaderMapper.kt:40)
	at com.passbolt.mobile.android.core.networking.ResponseHandler.parseErrorResponseBody(ResponseHandler.kt:79)
	at com.passbolt.mobile.android.core.networking.ResponseHandler.handleException(ResponseHandler.kt:46)
	at com.passbolt.mobile.android.passboltapi.registration.MobileTransferRepository.turnPage(MobileTransferRepository.kt:94)
	at com.passbolt.mobile.android.passboltapi.registration.MobileTransferRepository$turnPage$1.invokeSuspend(Unknown Source:16)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
	at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:42)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
	Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@30507a4, Dispatchers.Main]
Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $
	at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:385)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:214)
	... 15 more
1:29:03 PM File logging tree planted

Hi @Karpfen Welcome to the forum!

Did you see this yet? Passbolt Help | How to import SSL certificate on mobile application

Hi, yes but this unfortunately only targets SSL server certificates to handle transport encryption, not SSL client certificates used to authenticate/indentify myself to the server (cloudflare in this case).

I have used the same method (expect i installed as VPN & App certificate) to add my certificate to the device certificate store, and as i mentioned, it allows me to use the client certificate with multiple mobile browsers (chrome/brave).

It’s not needed of course unless you are using a self-signed cert on the server.

So, what cert is on the server currently?

The passbolt server is currently running without any certificate behind a nginx proxy that terminates TLS.

You can picture the connection like this:
User → (cloudflare TLS + cloudflare Client cert) → Cloudflare Proxy → (cloudflare TLS) → nginx proxy → (HTTP) → Passbolt server in internal network

My issue is purely related to the app’s capability to use and send a client certificate with requests.

:slightly_smiling_face:
From the link:
image

I am sorry but this is besides the point. I could have the same setup with Passbolt using an additional TLS connection between the nginx proxy and itself and the issue would be the same.

Correct me if I am misunderstanding (it happens), but it seems to me your device is not failing to connect due to the app not using your device cert store as you suggest, but instead its failing due to the server not serving https.

As this is the Installation Issues section, I was assuming your ultimate objective is to get the mobile app connected. With that in mind, the passbolt server must have https enabled to be able to use the passbolt mobile app.

Yes i believe there is a misunderstanding. For all intents and purposes the Passbolt instance appears to all clients with a valid TLS encryption.

Here is a brief summary of what client certificates are used for:

In this case i am using it to allow only bearers of the client certificate to even attempt to make connections to the Passbolt instance.

This test setup was purely meant to explore if Passbolt fits the use-cases of the company I’m working at. So i did not go through great lengths to tidy up within the internals.

Support for client certificates although not critical (since it only seems to lack in the mobile app) would be very useful.

In order to test your use case, I think you should start with the minimum requirements met as documented, so that mobile works. Then see how the additional requirements are working.

The Feature Requests category may be the place to post about client certificates if you think your scenario is not addressed, based on your findings.

Thanks for taking the time to investigate it!

Thanks, i might have missed the category. I was just initially unsure if it was a bug or i was missing something.
I will consider making a feature request out of this instead :slight_smile:

1 Like

The backend app’s config is leading to it’s enabling/disabling of mobile traffic. So I think what you are attempting should work, but not in place of the existing requirements.