nextcloud/android

๐Ÿ”‘ Support for client certificates

Closed this issue ยท 53 comments

Actual behaviour

In order to secure Nextcloud on TLS level, it would be good if the app could support client certificates. If the client certificate is not sent on handshake, the server prevents access to Nextcloud logon page. This would provide a second line of defense.

Expected behaviour

Nextcloud app should support client certificates as other apps like caldav sync and carddav sync already do.

Hello mlsxlist,

if this is of any use to the case, there is an open issue about this over at the owncloud github site:

owncloud/android#163

Best wishes
riesnerd

I would vote this "medium" to get this done when we (@mario or me) have time.
Everything that enhances the security of the app/nextcloud should have priority.
cc @mario ?

Okay thanks! In some way this also enhances security. ;)

mario commented

@tobiasKaminsky let me think about it, and I'll come back to you - we need to focus on fixing current issues first to make the app (more) usable than it is now - thought definitely this is something we want to do in the future.

I would also very much like to see this, because it protects from potential issues with the login page as well as weak passwords and password guessing attacks.

Any news on this ? This is a very important enhancement !

@mario let me know if you still need a test setup with client certificates enabled. In the meantime I will try to look into this (I haven't worked on android apps yet, but I can find my way around in php/java/python so I will give it a try)

mario commented

@AndreasMettlen yes I do. Send me the required cert + url, server and pass to mario@nextcloud.com :)

@mario Did you have a chance yet to look into this ?

@mario Did the second certificate and the talk app help you in making progress on this issue ?
Anything I can do to support ?

mario commented

@AndreasMettlen I implemented the initial support for client cert in Talk app. Now I need to validate it works which is why I asked for the second one. I'll try to do that on Friday.

If it works, I can see how easy/hard it is to put it into the Files (this) app.

Hi,
I really look forward for this enhancement.
In the meantime I've tried to patch the code on Android and it kinda works... even if it's ugly as hell ๐Ÿ˜ˆ
I've used the Android KeyChain features to allow the user to select a PKCS12 certificate and then stored the cert alias into the app preferences:

AuthenticatorActivity

private void checkOcServer() {
        if (mHostUrlInput != null) {
            uri = mHostUrlInput.getText().toString().trim();
            mOkButton.setEnabled(false);
            showRefreshButton(false);
        } else {
            uri = mServerInfo.mBaseUrl;
        }

        mServerIsValid = false;
        mServerIsChecked = false;
        mServerInfo = new GetServerInfoOperation.ServerInfo();

        if (uri.length() != 0) {
            if (mHostUrlInput != null) {
                uri = AuthenticatorUrlUtils.stripIndexPhpOrAppsFiles(uri);
                mHostUrlInput.setText(uri);
            }

            // Handle internationalized domain names
            try {
                uri = DisplayUtils.convertIdn(uri, true);
            } catch (IllegalArgumentException ex) {
                // Let Owncloud library check the error of the malformed URI
                Log_OC.e(TAG, "Error converting internationalized domain name " + uri, ex);
            }

            if (mHostUrlInput != null) {
                mServerStatusText = getResources().getString(R.string.auth_testing_connection);
                mServerStatusIcon = R.drawable.progress_small;
                showServerStatus();
            }

            if (NetworkUtils.alias == null) {
                KeyChain.choosePrivateKeyAlias(this, (KeyChainAliasCallback) this, null, null, uri, -1, null);
            } else {
                startServerInfoIntent();
            }

        } else {
            mServerStatusText = "";
            mServerStatusIcon = 0;
            if (!webViewLoginMethod) {
                showServerStatus();
            }
        }
    }
public void alias(final String alias) {
        System.out.println("THREAD: " + Thread.currentThread().getName());
        System.out.println("Selected alias: " + alias);
        try {
            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this.getApplicationContext());
            SharedPreferences.Editor editor = sp.edit();
            editor.putString("TLS_ALIAS", alias);
            editor.commit();

            startServerInfoIntent();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Then it uses the cert to initialize the SslSocketFactory:

NetworkUtils (nextcloud-android-library)

public static AdvancedSslSocketFactory getAdvancedSslSocketFactory(Context context) 
    		throws GeneralSecurityException, IOException {

        if (mAdvancedSslSocketFactory  == null) {

            if(chain == null || pk == null || alias == null) {
                // try to recover from preferences
                SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
                alias = sp.getString("TLS_ALIAS", "");
                try {
                    chain = KeyChain.getCertificateChain(context, alias);
                    pk = KeyChain.getPrivateKey(context, alias);
                } catch (KeyChainException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (NullPointerException e) {
                    e.printStackTrace();
                }
            }

            if(chain != null) {
                KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());

                X509ExtendedKeyManager keyManager = new X509ExtendedKeyManager() {

                    @Override
                    public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
                        return alias;
                    }

                    @Override
                    public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
                        return alias;
                    }

                    @Override
                    public X509Certificate[] getCertificateChain(String s) {
                        return chain;
                    }

                    @Override
                    public String[] getClientAliases(String s, Principal[] principals) {
                        return new String[]{alias};
                    }

                    @Override
                    public String[] getServerAliases(String s, Principal[] principals) {
                        return new String[]{alias};
                    }

                    @Override
                    public PrivateKey getPrivateKey(String s) {
                        return pk;
                    }
                };

                TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
                trustFactory.init(trustStore);
                TrustManager[] trustManagers = trustFactory.getTrustManagers();

                X509TrustManager[] tm = new X509TrustManager[] { new AdvancedX509TrustManager(trustStore) {

                    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    }

                    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    }

                    public X509Certificate[] getAcceptedIssuers() {
                        return chain;
                    }

                    public boolean isClientTrusted(X509Certificate[] arg0) {
                        return true;
                    }

                    public boolean isServerTrusted(X509Certificate[] arg0) {
                        return true;
                    }

                } };

                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(new KeyManager[] {keyManager}, tm, null);
                SSLContext.setDefault(sslContext);

                mHostnameVerifier = new BrowserCompatHostnameVerifier();
                mAdvancedSslSocketFactory = new AdvancedSslSocketFactory(sslContext, new AdvancedX509TrustManager(trustStore), mHostnameVerifier);

            } else {
                // can't recover the chain
                ...
            }
        }
        return mAdvancedSslSocketFactory;
    }

That's a big paste-up of various articles and posts that I've read to try to solve the problem... unfortunately I can't remember all of them:
owncloud/android#163
kbremner

Hope it helps.
Best wishes

mario commented

@mbrescia feel free to start a patch, and I can help? In the mean time, maybe you can try Nextcloud Talk v1.2.0beta? Same for you @AndreasMettlen ^_^

ClCfe commented

Hello @mario
i just updated to 2.0.0 from play store
I ve got this error as soon as I enter the server address
attempt to invoke virtual method java.lang.String com.nextcloud.talk.models.database.UserEntity.getClientCertificate() on a null object reference

I have android 8.1.0

mario commented

@ClCfe can you file a bug here, preferably with stacktrace?

https://github.com/nextcloud/talk-android

@mbrescia
is there any chance that you upload your whole example, because i tried your workaround but without success.
best regards

@proton2b
Sure, I'll start a branch as soon as I can

@mbrescia
ok great, thanks in advance!

Has there been any update on this? It would be an excellent addition to security

Has there been any update on this? It would be an excellent addition to security

Unfortunately not.

mirko commented

I'm really interested as well. Actually I already setup the server and my desktop client that way and was kind of puzzled the Android app refused to work with client certificates.

I would also like to see client certificates supported in the nextcloud app.

+1

Instead of adding +1 comments to this ticket please use the thumb up emoji at the top. Thanks.

I know its not directly related but can someone share an Apache config for Collabora to communicate with Nextcloud when SSLVerifyClient require is set in Apache?

I've got it set on both the NextCloud and Collabora sub domains, I can authenticate fine via the certificate to both subdomains but Collabora won't work.

It's almost like the Collabora sub domain needs a copy of the client cert to communicate with the nextcloud sub domain. Am I on the right path?

@ben423423n32j14e please don't hijack issues like that, this is not a support forum. I recommend you first have a look at https://github.com/nextcloud/richdocuments if you can find any info.

I've found an alternative to certificate authentication for the Android app (but still keeping certificate authentication for the website and Collabora).

Basically I compiled my own version of the NextCloud Android app and replaced the http user agent with a really long random string.

If certificate authentication is not successful, an Apache re-write rule activates and if it can see the custom user agent from the app (being transmitted over https), it will allow bypassing the certificate authentication.

Note that the app must already be logged into Nextcloud before activating the Apache rewrite rule (not sure why).

@stephanritscher seems to have made a working patch.
Why hasn't it been integrated yet ?

+1 vote for client cert auth from me! please!

An alternative approach is to make NextCloud an OAuth2 client, then implement an OAuth2 authz/token endpoint that uses client cert for authentication. I am not sure if NextCloud can function as an OAuth2 client out-of-the-box, but there is already Google login, e.g. in Social Login, so that is certainly possible.

Perhaps I should take a crack at this.

An alternative approach is to make NextCloud an OAuth2 client, then implement an OAuth2 authz/token endpoint that uses client cert for authentication. I am not sure if NextCloud can function as an OAuth2 client out-of-the-box, but there is already Google login, e.g. in Social Login, so that is certainly possible.

Perhaps I should take a crack at this.

I am not sure to understand of the advantages of this method over just using the device certificate truststore through the Android API, just like web browser would do.

In my case (but I'm sure this would perfectly fit compagnies security needs too), the setup is the following : a web fronting reverse-proxy which authorize access to the nextcloud server only if the device present a valid client certificate. The goal behind all that is to avoid having an useless and huge attack surface fronting on the internet. And when I see the activity of the nextcloud's bug bounty program, I prefer to close as many doors as I can by myself.

Thus, I am not sure to understand correctly the architecture of "NextCloud as an OAuth2 client", but if nextcloud has to be fronting on the internet, we probably miss the point :(

Configuring web server to require client certificate as an additional layer of security is fine. I thought however some installations wanted to use client cert only, without the regular username/password. Perhaps there is some well defined mechanism to bypass that; I am new to NextCloud so just unaware of such.

As for "NextCloud as an OAuth2 client", it can be confusing. I'm not talking about NextCloud as an OAuth2 client to access say Google Drive. Rather, NextCloud can leverage the OAuth2 authorization flow as a unified way to support arbitrary authentication mechanisms. Consider what happens when a NextCloud user logs in via Google:

  1. NextCloud user clicks the "Google Login" button on the NextCloud login page.
  2. Some Google controlled webflow is kicked off that authenticates the user with Google.
  3. The webflow ends with a callback from Google to NextCloud with an authorization result. NextCloud verifies the authorization and lets the user in.

We can replace 'Google' above with 'My Special'. The 'My Special' webflow can be arbitrarily complicated -- it can do client cert, captcha, SMS code, etc., even combination of them. Core NextCloud is out of the loop and would not need additional development just to support new auth mechanisms.

... without the regular username/password. Perhaps there is some well defined mechanism to bypass that; I am new to NextCloud so just unaware of such.

There is SSO & SAML authentication that is relevant too. In particular it looks like any server environment variable based mechanism would work.

This issue is a stopper for me. I need just a simple client cert validation on https level, independent of user authentication. Any plans to implement it?

cp289 commented

If I understand correctly, implementing this feature in the android-library repo would be a more effective solution since it would add client certificate support to all Nextcloud apps using the library. There is an issue for this here.

tdotu commented

Is there any update on this issue? I would love to have client certificates on Android.
Due to the following issue thread, I assume the desktop client supports it: nextcloud/desktop#1190
But I haven't tested it yet.

Not sure if it helps anybody but client certificates work for me in 3.2.1.
However, what I found out is that as soon as I let my nginx Reverse Proxy send the HSTS headers (Strict-Transport-Security), I do get an error and Nextcloud is not connecting anymore. Not sure if this is intended (because HSTS is somehow redundant there) or a bug.

Not sure if it helps anybody but client certificates work for me in 3.2.1.

You probably talk about the desktop client. This ticket is about the Android client.

Using cloudflare DNS with client cert required for protection, I really need to have the client cert requirement handled by the android app

Just to recall the request for the client certificates support.

It's a request that was open since the Jan 29, 2017 but it hasn't been really considered yet, although it would be a considerable bump up in security for the Nextcloud services that can't be simply exposed to the Internet due to internal policies.

Why is this feature not getting into nextcloud or even other apps like home assistant. Is it because is hard to implement? or because people does not need it in general? I find mTLS a really simple and secure option for non-expert users...

@stephanritscher - Thanks it worked - well worth the APK compilation time!!!
There is one issue which may be due to my setup (mTLS internet facing/TLS internal) I just had to register internally, it was not working externally.

The selection UI should really not been seen as a blocker if we consider the security value brought by this feature.
Thanks again you made my day!

@mario Is there anyway to bring this to at least the 'dev' version?

Is there anyway to bring this to at least the 'dev' version?

If you want to help us integrating this feature, then please create a PR both on this app and on library, so we can review and discuss it.

@stephanritscher - Thanks it worked - well worth the APK compilation time!!! There is one issue which may be due to my setup (mTLS internet facing/TLS internal) I just had to register internally, it was not working externally.

The selection UI should really not been seen as a blocker if we consider the security value brought by this feature. Thanks again you made my day!

@mario Is there anyway to bring this to at least the 'dev' version?

Initial registration fails to me too (after workaround and registering, connects fine). was able to investigate a little bit:
it is faulting here:
log from the nginx:
ocs/v2.php/cloud/user?format=json HTTP/2.0" 444 0 "-" "Mozilla/5.0 (Android) Nextcloud-android/20220921"

tried to dig further (logcat) and this seems as possible code place:

android-library/library/src/main/java/com/owncloud/android/lib/resources/user/GetUserInfoRemoteOperation.java

public RemoteOperationResult run(NextcloudClient client) {
RemoteOperationResult result;
int status;
GetMethod get = null;

    String url = client.getBaseUri() + OCS_ROUTE_SELF;

    // get the user
    try {

        get = new GetMethod(url, true);
        HashMap<String, String> map = new HashMap<>();
        map.put("format", "json");

        get.setQueryString(map);
        status = client.execute(get);

Looks like we have multiple commits from Stephan (thank you, by the way).

@stephanritscher can you please issue a PR so we can get this implemented in the official Nextcloud Android client? It sounds like @tobiasKaminsky is onboard. I can't think of a more vitally significant piece of development for the Nextcloud community. Security of a file storage/synchronization platform is always highest priority.

Thank you again for your work.

@stephanritscher can you please issue a PR so we can get this implemented in the official Nextcloud Android client?

I rebased my repos and created PRs nextcloud/android-library#1005 and #11099 to start the discussion. I guess it might need some changes, but let's see.

Sorry, but I might not be able to respond/incorporate changes quickly due too lack of time.

@AlvaroBrey in #11099 it was mentioned that there was an internal consensus,
would you mind describing a bit more? I have a bit of spare time over the coming break and would like to understand what would be accepted?

Just for reference major open source application in the IoT world has used similar method:
home-assistant/android#2023
Mattermost support this due to the use of react.
Not to forget Talk (and that is working great!)

@AlvaroBrey in #11099 it was mentioned that there was an internal consensus,
would you mind describing a bit more? I have a bit of spare time over the coming break and would like to understand what would be accepted?

As outlined in my original comment in 11099, we're just not comfortable using a third-party library for this purpose. It should all (logic, state and UI) be contained in our app or our library. Additionally it would be great if the solution could somewhat be aligned to the one Talk already uses, in hopes of a future reuse between apps.

If you want to contribute but are unsure of the approach, feel free to open a draft PR and ping me, so I can guide you during the development and not just at the end of it :)

Any news on the topic? Still not implemented but would be awesome to have!

Done with
#12408
nextcloud/android-library#1308

Huge thanks to @Elv1zz who did 99,95% of the work ๐ŸŽ‰

Any plan on adding it to release?

It's planned to ship it with the next feature release 3.29 - scheduled for 24th April