Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Wear OS TLS client certificate authentication (TLS CCA) support #3924

Merged
merged 6 commits into from
Dec 13, 2023

Conversation

kleest
Copy link
Contributor

@kleest kleest commented Oct 8, 2023

Summary

Wear OS does not currently allow the user to install certificates to the system-wide KeyChain for TLS CCA support. This commit adds support for using certificates from the app-specific Android KeyStore with UI for setting up a certificate during the Wear OS onboarding process. The manual step in the onboarding process is required since we cannot transmit certificates of the Android KeyChain because they are not extractable.

In particular, this commit adds the following changes:

  • KeyStoreImpl as an additional KeyChainRepository interface implementation for loading and storing keys to the application's KeyStore. TLSHelper uses KeyStoreImpl as a fallback key manager.
  • UI for selecting a certificate file with GET_CONTENT intent during Wear OS onboarding in OnboardingActivity if it is detected that the Home Assistant may require TLS CCA. The UI includes a password check for the PKCS12 container.
  • During onboarding the app sends the raw PKCS12 data to Wear OS together with the container password. The connection is assumed to be encrypted and trusted so that no additional encryption is necessary.

Screenshots

Onboarding with certificate selection (light mode):

Onboarding with certificate selection (dark mode):

Onboarding without certificate selection (no changes):

Link to pull request in Documentation repository

Documentation: home-assistant/companion.home-assistant#995

Any other notes

This is a fix #2771. The linked issues also contains the idea to generate a certificate signing request on the Wear OS device, however, this PR does not add support for this feature.

@kleest
Copy link
Contributor Author

kleest commented Oct 8, 2023

Fixed a bug where further onboarding fails if the user previously logged out from the WearOS app.

Copy link
Member

@jpelgrom jpelgrom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Onboarding a watch is currently is blocked if you have any server with mTLS, but do not choose a certificate for the watch. This is incorrect because you may use a different server on the watch than the one requiring a certificate.

@kleest
Copy link
Contributor Author

kleest commented Oct 14, 2023

Onboarding a watch is currently is blocked if you have any server with mTLS, but do not choose a certificate for the watch. This is incorrect because you may use a different server on the watch than the one requiring a certificate.

Good point, I did not think about it. I changed the logic.

I added changes in a new commit instead of amending for easier reviewing. Before merging this PR into the master branch, I would like to squash the commits to have a clean history.

@jpelgrom
Copy link
Member

Thanks! I'll have a look at the changes and test it soon.

Before merging this PR into the master branch, I would like to squash the commits to have a clean history.

All pull requests are squashed when merged, you don't need to do this yourself (in fact, that is preferred, as otherwise it makes it harder to review).

@kleest
Copy link
Contributor Author

kleest commented Oct 14, 2023

But the squashing defaults do not retain the commit descriptions but only titles. The descriptions adds quite a bit of context to this PR. That's why I explicitly mentioned it 🙂

@jpelgrom
Copy link
Member

We use the squash & merge option on pull requests to merge which keeps titles + descriptions for each commit, see b0ad11d for example. Your pull request with all discussion and the branch at the time of merging will also remain available on GitHub.


@HiltAndroidApp
open class HomeAssistantApplication : Application() {
private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO + Job())
Copy link
Contributor

@marazmarci marazmarci Oct 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When creating a long-lived CoroutineScope (in this case, tied to the lifetime of HomeAssistantApplication), it's better to use SupervisorJob, as the scope won't get canceled forever if one coroutine fails. Now ioScope is only used at a single place, but in the future, others can also start using it, so it would be better to prepare.

Suggested change
private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO + Job())
private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I understood this correctly, CoroutineScope(Dispatchers.IO + Job()) is already used 7x in "common" and "app" modules in the source code. The new addition is this line in the "wear" module. Should all other occurrences be changed to SupervisorJob() as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marazmarci While you're not wrong let's not change a common pattern in this PR, that's more (unrelated) changes. Feel free to make that change in another PR. Using a 'normal' job works if you're aware of it and handle exceptions.

Copy link
Member

@jpelgrom jpelgrom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While trying to test this, I encountered the following exception (on a Galaxy Watch 4 with Wear OS 4):

2023-10-22 15:09:55.263 15373-15373 PhoneSettingsListener   io....stant.companion.android.debug  E  Cannot load TLS client certificate
                                                                                                    java.io.IOException: error constructing MAC: java.security.InvalidKeyException: No installed provider supports this key: com.android.org.bouncycastle.jcajce.PKCS12Key
                                                                                                    	at com.android.org.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi.engineLoad(PKCS12KeyStoreSpi.java:852)
                                                                                                    	at java.security.KeyStore.load(KeyStore.java:1505)
                                                                                                    	at io.homeassistant.companion.android.phone.PhoneSettingsListener$login$1.invokeSuspend(PhoneSettingsListener.kt:160)
                                                                                                    	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
                                                                                                    	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
                                                                                                    	at android.os.Handler.handleCallback(Handler.java:942)
                                                                                                    	at android.os.Handler.dispatchMessage(Handler.java:99)
                                                                                                    	at android.os.Looper.loopOnce(Looper.java:201)
                                                                                                    	at android.os.Looper.loop(Looper.java:288)
                                                                                                    	at android.app.ActivityThread.main(ActivityThread.java:7962)
                                                                                                    	at java.lang.reflect.Method.invoke(Native Method)
                                                                                                    	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:550)
                                                                                                    	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:952)

Could it be that not all watches support this? If so we should detect + send that info to the phone in advance to make the user understand why it doesn't work.

Also a few comments to clean up the code and improve the user experience.

@kleest
Copy link
Contributor Author

kleest commented Oct 22, 2023

@jpelgrom Thanks for your comments!

While trying to test this, I encountered the following exception (on a Galaxy Watch 4 with Wear OS 4):

2023-10-22 15:09:55.263 15373-15373 PhoneSettingsListener   io....stant.companion.android.debug  E  Cannot load TLS client certificate
                                                                                                    java.io.IOException: error constructing MAC: java.security.InvalidKeyException: No installed provider supports this key: com.android.org.bouncycastle.jcajce.PKCS12Key
                                                                                                    	at com.android.org.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi.engineLoad(PKCS12KeyStoreSpi.java:852)
                                                                                                    	at java.security.KeyStore.load(KeyStore.java:1505)
                                                                                                    	at io.homeassistant.companion.android.phone.PhoneSettingsListener$login$1.invokeSuspend(PhoneSettingsListener.kt:160)
                                                                                                    	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
                                                                                                    	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
                                                                                                    	at android.os.Handler.handleCallback(Handler.java:942)
                                                                                                    	at android.os.Handler.dispatchMessage(Handler.java:99)
                                                                                                    	at android.os.Looper.loopOnce(Looper.java:201)
                                                                                                    	at android.os.Looper.loop(Looper.java:288)
                                                                                                    	at android.app.ActivityThread.main(ActivityThread.java:7962)
                                                                                                    	at java.lang.reflect.Method.invoke(Native Method)
                                                                                                    	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:550)
                                                                                                    	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:952)

Could it be that not all watches support this? If so we should detect + send that info to the phone in advance to make the user understand why it doesn't work.

Also a few comments to clean up the code and improve the user experience.

Quick answer regarding the KeyStore exception: This is strange. The PKCS12 store type should be supported on Android since API Level 1 (see https://developer.android.com/reference/java/security/KeyStore) and it works perfectly fine on the emulator. I am also quite confused because there seems to exist a PKCS12 implementation, note the class com.android.org.bouncycastle.jcajce.PKCS12Key, but it throws an error during MAC calculation. I don't want to say (yet) that this is a ROM issue, but the exception is quite bizarre 🤨. I also have a Galaxy Watch 4 with Wear OS 4, so I will investigate this further locally.

I will have a look at your other comments in the next days.

@kleest
Copy link
Contributor Author

kleest commented Oct 29, 2023

@jpelgrom The PKCS12 problem is due to different PKCS12 encryption/signature algorithms (see SO thread for details). In my tests, I used a legacy PKCS12 container (pbeWithSHA1And3-KeyTripleDES-CBC) since the modern format (AES-256-CBC) already failed during password check on the phone (API level 33). It seems like your phone works with the new container, but then Wear OS (API level 33) fails opening the container. I did not consider the case that it works on the phone but does not work on Wear OS. Are you using API level 34 on the phone and a modern keystore format (check with openssl pkcs12 -info -in user.p12 -info -nokeys)?

I am also unsure how to handle this problem. I see two options:

  1. Check the PKCS12 container format on the phone and fail early: If it uses the modern format, reject it since there is no Wear OS version that supports this format. Disadvantage: A new Wear OS version may support the modern format. Then, we need to change the code.
  2. Add error handling on Wear OS and fail late. Disadvantage: A user has to restart the onboarding process again.

I prefer option 2.

Apart from these options, I am not sure if we can detect if opening the container fails due to an unsupported format or the wrong key, but I'm afraid we can't. The user certificate import system dialog on the phone already has the issue that it fails with a wrong password if you try to import a modern format PKCS12 container on API level <= 33, even though the user provided the correct password.

@jpelgrom
Copy link
Member

jpelgrom commented Nov 1, 2023

Are you using API level 34 on the phone and a modern keystore format (check with openssl pkcs12 -info -in user.p12 -info -nokeys)?

Yes and I think so? Not very familiar with all the details. Command output mentions what you describe as modern format:

MAC: sha256, Iteration 2048
MAC length: 32, salt length: 8
PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256

(...)

PKCS7 Data
Shrouded Keybag: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256

I prefer option 2.

I agree. However, could we maybe extend this to the phone as that is most likely where the user will see that the login failed? It already sends over the stacktrace to the phone (as text) as that is what causes the 'Failed to login' snackbar to show.

This might be worth mentioning in the documentation, too much information and too technical to put in the app strings.

@virtualdj
Copy link

Sorry for the late reply, but I totally missed the notification on the other thread #2771.
This PR is working GREAT!!!! 🎉

I downloaded the artifact from here and installed the Android APK on the phone and the Wear APK on my Galaxy Watch 4 with Wear OS 4.

I made two tests, one with a wrong certificate (actuallly revoked) and another with the correct one; both are PFX files without any password. Then I captured the logs on the Wear OS device from the PC with:

[PC] # adb logcat keystore2:* ServerConnectionInfo:* okhttp.OkHttpClient:* PhoneSettingsListener:* AuthRepo:* *:S

First test: revoked certificate ✔

I first tried using the expired certificate to check if an error message was printed.

Phone app when loading expired certificate

11-08 09:45:25.163  6161  9883 D PhoneSettingsListener: onDataChanged 1
11-08 09:45:25.866   310  7323 I keystore2: keystore2::security_level: In import_key. 10046, Some("TLSClientCertificate")
11-08 09:45:25.871   310  7323 I keystore2: keystore2::database: In store_new_key "TLSClientCertificate", uid=10046, cert=true, cert_chain=false rebound=true
11-08 09:45:25.885   310  9885 I keystore2: keystore2::gc: In process_one_key: Trying to invalidate key blob. Km(Not Support)
11-08 09:45:25.902   310   320 I keystore2: keystore2::service: In update_subcomponent. 10046, None
11-08 09:45:25.934  6161  6161 D ServerConnectionInfo: localUrl is: false, usesInternalSsid is: false, usesWifi is: true
11-08 09:45:25.934  6161  6161 D ServerConnectionInfo: Using external URL
11-08 09:45:25.941  6161  9886 I okhttp.OkHttpClient: --> POST https://homeassistant.mydomain.duckdns.org/auth/token
11-08 09:45:25.941  6161  9886 I okhttp.OkHttpClient: Content-Type: application/x-www-form-urlencoded
11-08 09:45:25.942  6161  9886 I okhttp.OkHttpClient: Content-Length: 119
11-08 09:45:25.943  6161  9886 I okhttp.OkHttpClient: grant_type=authorization_code&code=c927049e39b24eb89ddb9736540471f1&client_id=https%3A%2F%2Fhome-assistant.io%2Fandroid
11-08 09:45:25.943  6161  9886 I okhttp.OkHttpClient: --> END POST (119-byte body)
11-08 09:45:27.192   310   320 I keystore2: keystore2::security_level: In create_operation. Success to create IKeystoreOperation. 10046, None
11-08 09:45:27.318  6161  9886 I okhttp.OkHttpClient: <-- 400 https://homeassistant.mydomain.duckdns.org/auth/token (1374ms)
11-08 09:45:27.318  6161  9886 I okhttp.OkHttpClient: server: openresty
11-08 09:45:27.318  6161  9886 I okhttp.OkHttpClient: date: Wed, 08 Nov 2023 08:45:24 GMT
11-08 09:45:27.318  6161  9886 I okhttp.OkHttpClient: content-type: text/html
11-08 09:45:27.319  6161  9886 I okhttp.OkHttpClient: content-length: 212
11-08 09:45:27.320  6161  9886 I okhttp.OkHttpClient: <html>
11-08 09:45:27.320  6161  9886 I okhttp.OkHttpClient: <head><title>400 The SSL certificate error</title></head>
11-08 09:45:27.320  6161  9886 I okhttp.OkHttpClient: <body>
11-08 09:45:27.320  6161  9886 I okhttp.OkHttpClient: <center><h1>400 Bad Request</h1></center>
11-08 09:45:27.320  6161  9886 I okhttp.OkHttpClient: <center>The SSL certificate error</center>
11-08 09:45:27.320  6161  9886 I okhttp.OkHttpClient: <hr><center>openresty</center>
11-08 09:45:27.320  6161  9886 I okhttp.OkHttpClient: </body>
11-08 09:45:27.320  6161  9886 I okhttp.OkHttpClient: </html>
11-08 09:45:27.321  6161  9886 I okhttp.OkHttpClient: <-- END HTTP (212-byte body)
11-08 09:45:27.324  6161  6161 E PhoneSettingsListener: Unable to login to Home Assistant
11-08 09:45:27.324  6161  6161 E PhoneSettingsListener: retrofit2.HttpException: HTTP 400
11-08 09:45:27.324  6161  6161 E PhoneSettingsListener:         at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:53)
11-08 09:45:27.324  6161  6161 E PhoneSettingsListener:         at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
11-08 09:45:27.324  6161  6161 E PhoneSettingsListener:         at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
11-08 09:45:27.324  6161  6161 E PhoneSettingsListener:         at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
11-08 09:45:27.324  6161  6161 E PhoneSettingsListener:         at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
11-08 09:45:27.324  6161  6161 E PhoneSettingsListener:         at java.lang.Thread.run(Thread.java:1012)
11-08 09:45:27.329  6161  6161 D ServerConnectionInfo: localUrl is: false, usesInternalSsid is: false, usesWifi is: true
11-08 09:45:27.329  6161  6161 D ServerConnectionInfo: Using external URL
11-08 09:45:27.329  6161  6161 E AuthRepo: Unable to revoke session.
11-08 09:45:27.397  6161  6161 D PhoneSettingsListener: Successfully sent /loginResult to device
11-08 09:45:27.405  6161  6161 D PhoneSettingsListener: Successfully sent /config to device
11-08 09:45:42.227   310  9884 I keystore2: keystore2::watchdog: Watchdog thread idle -> terminating. Have a great day.
11-08 09:45:58.670  6161  9936 D PhoneSettingsListener: onDataChanged 1
11-08 09:45:59.380   310   320 I keystore2: keystore2::security_level: In import_key. 10046, Some("TLSClientCertificate")
11-08 09:45:59.382   310   320 I keystore2: keystore2::database: In store_new_key "TLSClientCertificate", uid=10046, cert=true, cert_chain=false rebound=true
11-08 09:45:59.393   310  9885 I keystore2: keystore2::gc: In process_one_key: Trying to invalidate key blob. Km(Not Support)
11-08 09:45:59.397   310  7323 I keystore2: keystore2::service: In update_subcomponent. 10046, None
11-08 09:45:59.430  6161  6161 D ServerConnectionInfo: localUrl is: false, usesInternalSsid is: false, usesWifi is: true
11-08 09:45:59.430  6161  6161 D ServerConnectionInfo: Using external URL
11-08 09:45:59.437  6161  9886 I okhttp.OkHttpClient: --> POST https://homeassistant.mydomain.duckdns.org/auth/token
11-08 09:45:59.437  6161  9886 I okhttp.OkHttpClient: Content-Type: application/x-www-form-urlencoded
11-08 09:45:59.438  6161  9886 I okhttp.OkHttpClient: Content-Length: 119
11-08 09:45:59.438  6161  9886 I okhttp.OkHttpClient: grant_type=authorization_code&code=f0eaf8d444db4fcaa29b9158368f6216&client_id=https%3A%2F%2Fhome-assistant.io%2Fandroid
11-08 09:45:59.439  6161  9886 I okhttp.OkHttpClient: --> END POST (119-byte body)
11-08 09:45:59.494  6161  9886 I okhttp.OkHttpClient: <-- 400 https://homeassistant.mydomain.duckdns.org/auth/token (54ms)
11-08 09:45:59.495  6161  9886 I okhttp.OkHttpClient: server: openresty
11-08 09:45:59.495  6161  9886 I okhttp.OkHttpClient: date: Wed, 08 Nov 2023 08:45:56 GMT
11-08 09:45:59.496  6161  9886 I okhttp.OkHttpClient: content-type: text/html
11-08 09:45:59.496  6161  9886 I okhttp.OkHttpClient: content-length: 212
11-08 09:45:59.499  6161  9886 I okhttp.OkHttpClient: <html>
11-08 09:45:59.500  6161  9886 I okhttp.OkHttpClient: <head><title>400 The SSL certificate error</title></head>
11-08 09:45:59.500  6161  9886 I okhttp.OkHttpClient: <body>
11-08 09:45:59.500  6161  9886 I okhttp.OkHttpClient: <center><h1>400 Bad Request</h1></center>
11-08 09:45:59.500  6161  9886 I okhttp.OkHttpClient: <center>The SSL certificate error</center>
11-08 09:45:59.500  6161  9886 I okhttp.OkHttpClient: <hr><center>openresty</center>
11-08 09:45:59.500  6161  9886 I okhttp.OkHttpClient: </body>
11-08 09:45:59.500  6161  9886 I okhttp.OkHttpClient: </html>
11-08 09:45:59.501  6161  9886 I okhttp.OkHttpClient: <-- END HTTP (212-byte body)
11-08 09:45:59.508  6161  6161 E PhoneSettingsListener: Unable to login to Home Assistant
11-08 09:45:59.508  6161  6161 E PhoneSettingsListener: retrofit2.HttpException: HTTP 400
11-08 09:45:59.508  6161  6161 E PhoneSettingsListener:         at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:53)
11-08 09:45:59.508  6161  6161 E PhoneSettingsListener:         at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
11-08 09:45:59.508  6161  6161 E PhoneSettingsListener:         at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
11-08 09:45:59.508  6161  6161 E PhoneSettingsListener:         at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
11-08 09:45:59.508  6161  6161 E PhoneSettingsListener:         at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
11-08 09:45:59.508  6161  6161 E PhoneSettingsListener:         at java.lang.Thread.run(Thread.java:1012)
11-08 09:45:59.517  6161  6161 D ServerConnectionInfo: localUrl is: false, usesInternalSsid is: false, usesWifi is: true
11-08 09:45:59.517  6161  6161 D ServerConnectionInfo: Using external URL
11-08 09:45:59.519  6161  6161 E AuthRepo: Unable to revoke session.
11-08 09:45:59.642  6161  6161 D PhoneSettingsListener: Successfully sent /loginResult to device
11-08 09:45:59.654  6161  6161 D PhoneSettingsListener: Successfully sent /config to device
11-08 09:46:14.592   310  9937 I keystore2: keystore2::watchdog: Watchdog thread idle -> terminating. Have a great day.

Second test: correct certificate ✔

Then the correct certificate, which is the one already working on the phone with the HA app.

Phone app when loading the correct certificate

11-08 09:48:57.642  6161 10174 D PhoneSettingsListener: onDataChanged 1
11-08 09:48:58.310   310   320 I keystore2: keystore2::security_level: In import_key. 10046, Some("TLSClientCertificate")
11-08 09:48:58.312   310   320 I keystore2: keystore2::database: In store_new_key "TLSClientCertificate", uid=10046, cert=true, cert_chain=false rebound=true
11-08 09:48:58.323   310 10176 I keystore2: keystore2::gc: In process_one_key: Trying to invalidate key blob. Km(Not Support)
11-08 09:48:58.341   310   320 I keystore2: keystore2::service: In update_subcomponent. 10046, None
11-08 09:48:58.367  6161  6161 D ServerConnectionInfo: localUrl is: false, usesInternalSsid is: false, usesWifi is: true
11-08 09:48:58.367  6161  6161 D ServerConnectionInfo: Using external URL
11-08 09:48:58.374  6161 10179 I okhttp.OkHttpClient: --> POST https://homeassistant.mydomain.duckdns.org/auth/token
11-08 09:48:58.374  6161 10179 I okhttp.OkHttpClient: Content-Type: application/x-www-form-urlencoded
11-08 09:48:58.375  6161 10179 I okhttp.OkHttpClient: Content-Length: 119
11-08 09:48:58.375  6161 10179 I okhttp.OkHttpClient: grant_type=authorization_code&code=5fc37cd48f7e4f249847b4a723f40238&client_id=https%3A%2F%2Fhome-assistant.io%2Fandroid
11-08 09:48:58.376  6161 10179 I okhttp.OkHttpClient: --> END POST (119-byte body)
11-08 09:48:59.986   310  7323 I keystore2: keystore2::security_level: In create_operation. Success to create IKeystoreOperation. 10046, None
11-08 09:49:00.160  6161 10179 I okhttp.OkHttpClient: <-- 200 https://homeassistant.mydomain.duckdns.org/auth/token (1784ms)
11-08 09:49:00.161  6161 10179 I okhttp.OkHttpClient: server: openresty
11-08 09:49:00.161  6161 10179 I okhttp.OkHttpClient: date: Wed, 08 Nov 2023 08:48:57 GMT
11-08 09:49:00.161  6161 10179 I okhttp.OkHttpClient: content-type: application/json
11-08 09:49:00.161  6161 10179 I okhttp.OkHttpClient: cache-control: no-store
11-08 09:49:00.161  6161 10179 I okhttp.OkHttpClient: pragma: no-cache
11-08 09:49:00.161  6161 10179 I okhttp.OkHttpClient: x-served-by: homeassistant.mydomain.duckdns.org
11-08 09:49:00.164  6161 10179 I okhttp.OkHttpClient: {"access_token":"** redacted **","token_type":"Bearer","refresh_token":"** redacted **","expires_in":1800,"ha_auth_provider":"homeassistant"}
11-08 09:49:00.164  6161 10179 I okhttp.OkHttpClient: <-- END HTTP (424-byte body)
11-08 09:49:00.232  6161  6161 D ServerConnectionInfo: localUrl is: false, usesInternalSsid is: false, usesWifi is: true
11-08 09:49:00.232  6161  6161 D ServerConnectionInfo: Using external URL
11-08 09:49:00.237  6161  6161 D ServerConnectionInfo: localUrl is: false, usesInternalSsid is: false, usesWifi is: true
11-08 09:49:00.237  6161  6161 D ServerConnectionInfo: Using external URL
11-08 09:49:00.843  6161 10191 I okhttp.OkHttpClient: --> POST https://homeassistant.mydomain.duckdns.org/api/mobile_app/registrations
11-08 09:49:00.843  6161 10191 I okhttp.OkHttpClient: Content-Type: application/json; charset=UTF-8
11-08 09:49:00.844  6161 10191 I okhttp.OkHttpClient: Content-Length: 343
11-08 09:49:00.844  6161 10191 I okhttp.OkHttpClient: Authorization: Bearer ** redacted **
11-08 09:49:00.844  6161 10191 I okhttp.OkHttpClient: {"app_id":"io.homeassistant.companion.android","app_name":"Home Assistant","app_version":"2023.11.1-beta.0.5+39bcf17 (2)","device_name":"Galaxy Watch4 (J3BW)","manufacturer":"samsung","model":"SM-R870","os_name":"Android","os_version":"33","supports_encryption":false,"app_data":{"push_websocket_channel":false},"device_id":"** redacted **"}
11-08 09:49:00.845  6161 10191 I okhttp.OkHttpClient: --> END POST (343-byte body)
11-08 09:49:01.051   310  7323 I keystore2: keystore2::security_level: In create_operation. Success to create IKeystoreOperation. 10046, None
11-08 09:49:01.224  6161 10191 I okhttp.OkHttpClient: <-- 201 https://homeassistant.mydomain.duckdns.org/api/mobile_app/registrations (378ms)
11-08 09:49:01.225  6161 10191 I okhttp.OkHttpClient: server: openresty
11-08 09:49:01.225  6161 10191 I okhttp.OkHttpClient: date: Wed, 08 Nov 2023 08:48:58 GMT
11-08 09:49:01.225  6161 10191 I okhttp.OkHttpClient: content-type: application/json
11-08 09:49:01.226  6161 10191 I okhttp.OkHttpClient: x-served-by: homeassistant.mydomain.duckdns.org
11-08 09:49:01.230  6161 10191 I okhttp.OkHttpClient: {"cloudhook_url":null,"remote_ui_url":null,"secret":null,"webhook_id":"46a1e550c84e684ebe4d1f08ba1f78ae417c0db6e5dc6f283e74ec834a8aee17"}
11-08 09:49:01.231  6161 10191 I okhttp.OkHttpClient: <-- END HTTP (137-byte body)
11-08 09:49:01.249  6161  6161 D ServerConnectionInfo: localUrl is: false, usesInternalSsid is: false, usesWifi is: true
11-08 09:49:01.607  6161 10191 I okhttp.OkHttpClient: --> POST https://homeassistant.mydomain.duckdns.org/api/webhook/46a1e550c84e684ebe4d1f08ba1f78ae417c0db6e5dc6f283e74ec834a8aee17
11-08 09:49:01.607  6161 10191 I okhttp.OkHttpClient: Content-Type: application/json; charset=UTF-8
11-08 09:49:01.607  6161 10191 I okhttp.OkHttpClient: Content-Length: 21
11-08 09:49:01.608  6161 10191 I okhttp.OkHttpClient: {"type":"get_config"}
11-08 09:49:01.608  6161 10191 I okhttp.OkHttpClient: --> END POST (21-byte body)
11-08 09:49:01.633  6161 10191 I okhttp.OkHttpClient: <-- 200 https://homeassistant.mydomain.duckdns.org/api/webhook/46a1e550c84e684ebe4d1f08ba1f78ae417c0db6e5dc6f283e74ec834a8aee17 (24ms)
11-08 09:49:01.633  6161 10191 I okhttp.OkHttpClient: server: openresty
11-08 09:49:01.633  6161 10191 I okhttp.OkHttpClient: date: Wed, 08 Nov 2023 08:48:58 GMT
11-08 09:49:01.633  6161 10191 I okhttp.OkHttpClient: content-type: application/json; charset=utf-8
11-08 09:49:01.634  6161 10191 I okhttp.OkHttpClient: content-length: 2966
11-08 09:49:01.634  6161 10191 I okhttp.OkHttpClient: x-served-by: homeassistant.mydomain.duckdns.org
11-08 09:49:01.635  6161 10191 I okhttp.OkHttpClient: {"latitude": 0, "longitude": 0, "elevation": 0, "unit_system": {"length": "km", "accumulated_precipitation": "mm", "mass": "g", "pressure": "Pa", "temperature": "\u00b0C", "volume": "L", "wind_speed": "m/s"}, "location_name": "Home", "time_zone": "Europe/Rome", "components": ["persistent_notification", "sensor.netatmo", "cloud", "frontend", "forecast_solar", "var", "tag", "automation",  "binary_sensor", "netatmo", "webhook"], "version": "2023.8.4", "theme_color": "#03A9F4", "entities": {"22827a8da70953f3": {"disabled": false}}}
11-08 09:49:01.636  6161 10191 I okhttp.OkHttpClient: <-- END HTTP (2966-byte body)
11-08 09:49:01.687  6161  6161 D ServerConnectionInfo: localUrl is: false, usesInternalSsid is: false, usesWifi is: true
11-08 09:49:01.687  6161  6161 D ServerConnectionInfo: Using external URL
11-08 09:49:01.701  6161 10193 I okhttp.OkHttpClient: --> GET https://homeassistant.mydomain.duckdns.org/api/websocket
11-08 09:49:01.701  6161 10193 I okhttp.OkHttpClient: User-Agent: Home Assistant/2023.11.1-beta.0.5+39bcf17-1 (Android 13; SM-R870)
11-08 09:49:01.702  6161 10193 I okhttp.OkHttpClient: Upgrade: websocket
11-08 09:49:01.702  6161 10193 I okhttp.OkHttpClient: Connection: Upgrade
11-08 09:49:01.703  6161  6161 D ServerConnectionInfo: localUrl is: false, usesInternalSsid is: false, usesWifi is: true
11-08 09:49:01.703  6161  6161 D ServerConnectionInfo: Using external URL
11-08 09:49:01.703  6161 10193 I okhttp.OkHttpClient: Sec-WebSocket-Key: naBYX0pAjqG9SGaAUA9pig==
11-08 09:49:01.703  6161 10193 I okhttp.OkHttpClient: Sec-WebSocket-Version: 13
11-08 09:49:01.703  6161 10193 I okhttp.OkHttpClient: Sec-WebSocket-Extensions: permessage-deflate
11-08 09:49:01.703  6161 10193 I okhttp.OkHttpClient: --> END GET
11-08 09:49:01.882   310  7323 I keystore2: keystore2::security_level: In create_operation. Success to create IKeystoreOperation. 10046, None
11-08 09:49:02.021  6161 10193 I okhttp.OkHttpClient: <-- 101 Switching Protocols https://homeassistant.mydomain.duckdns.org/api/websocket (317ms)
11-08 09:49:02.021  6161 10193 I okhttp.OkHttpClient: Server: openresty
11-08 09:49:02.021  6161 10193 I okhttp.OkHttpClient: Date: Wed, 08 Nov 2023 08:48:59 GMT
11-08 09:49:02.021  6161 10193 I okhttp.OkHttpClient: Content-Type: application/octet-stream
11-08 09:49:02.022  6161 10193 I okhttp.OkHttpClient: Connection: upgrade
11-08 09:49:02.022  6161 10193 I okhttp.OkHttpClient: Upgrade: websocket
11-08 09:49:02.022  6161 10193 I okhttp.OkHttpClient: Sec-WebSocket-Accept: /2AeKZC09ToKM+PUDJrxJoAh4DQ=
11-08 09:49:02.022  6161 10193 I okhttp.OkHttpClient: Sec-WebSocket-Extensions: permessage-deflate
11-08 09:49:02.022  6161 10193 I okhttp.OkHttpClient: <-- END HTTP
11-08 09:49:02.442  6161  6161 D ServerConnectionInfo: localUrl is: false, usesInternalSsid is: false, usesWifi is: true
11-08 09:49:02.480  6161  6161 D PhoneSettingsListener: Successfully sent /loginResult to device
11-08 09:49:02.621  6161  6161 D PhoneSettingsListener: Successfully sent /config to device
11-08 09:49:16.432   310 10175 I keystore2: keystore2::watchdog: Watchdog thread idle -> terminating. Have a great day.

Test tile ✔

Once connected, even when out of the Wi-Fi range, the watch can finally read data from the Home Assistant instance!

Home Assistant tile for Wear OS

I hope this PR will be merged quickly to the HA official branch!
Thanks @kleest for spending your spare time developing this feature... very much appreciated!

@kleest
Copy link
Contributor Author

kleest commented Nov 14, 2023

@jpelgrom Sorry for the late response!

I agree. However, could we maybe extend this to the phone as that is most likely where the user will see that the login failed? It already sends over the stacktrace to the phone (as text) as that is what causes the 'Failed to login' snackbar to show.

This might be worth mentioning in the documentation, too much information and too technical to put in the app strings.

I am not sure how to go forward with this. Wear OS sends back one status indicator (KEY_SUCCESS) and an optional exception message (LOGIN_RESULT_EXCEPTION) in case the login failed. Currently, the exception message is only logged and never shown to the user. If I remove the current catch (e: IOException) (which is already redundant due to outer general catch, I missed that when implementing it), the user will see the generic "Could not register watch" message as with any other exception. This fails gracefully and the phone logcat contains the exception, but the user cannot actually figure out what's the problem without looking at the logs: network connectivity? TLS certificate error? ...?
So, shall I add an additional user-friendly error message regarding the TLS error or keep it like this and just mention this possible problem in the documentation PR?

@jpelgrom
Copy link
Member

Sorry for the late response!

No problem.

So, shall I add an additional user-friendly error message regarding the TLS error or keep it like this and just mention this possible problem in the documentation PR?

Thinking about it more and considering what you wrote, maybe this is too much to explain in a single line snackbar and to add to the PR (scope creep), and docs is the better place for it. If this becomes more common, more detailed error handling can always be added later.

@virtualdj
Copy link

virtualdj commented Nov 15, 2023

but the user cannot actually figure out what's the problem without looking at the logs: network connectivity? TLS certificate error? ...? So, shall I add an additional user-friendly error message regarding the TLS error or keep it like this and just mention this possible problem in the documentation PR?

Personally, I would prefer having the snackbar message on the phone at least to suggest whether it was a network error or a TLS error. In fact, when I tried with the revoked certificate above and saw the Could not register watch error I initially though that the watch lost the connection. But it was "working" instead, only with the wrong certificate.

@vexofp
Copy link

vexofp commented Nov 22, 2023

I am very excited for this feature!

I tested the latest artifacts on my Galaxy S23 and Galaxy Watch 4, and everything worked flawlessly. (Similar to the test above, except with a password on the certificate file.) 🚀

Other than just testing, is there anything I can do to help push this over the finish line?

@kleest
Copy link
Contributor Author

kleest commented Nov 22, 2023

@vexofp Thanks for testing! I will need to change the exception handling and want to add a simple certificate expiry check on the phone before sending the certificate to the watch. I plan to finish this on the weekend, I am currently a little busy.

Wear OS does not currently allow the user to install certificates to the
system-wide KeyChain for TLS CCA support. This commit adds support for
using certificates from the app-specific Android KeyStore with UI for
setting up a certificate during the Wear OS onboarding process.
The manual step in the onboarding process is required since we cannot
transmit certificates of the Android KeyChain because they are not
extractable.

In particular, this commit adds the following changes:
* KeyStoreImpl as an additional KeyChainRepository interface
  implementation for loading and storing keys to the application's
  KeyStore. TLSHelper uses KeyStoreImpl as a fallback key manager.
* UI for selecting a certificate file with GET_CONTENT intent during
  Wear OS onboarding in OnboardingActivity if it is detected that the
  Home Assistant may require TLS CCA. The UI includes a password check
  for the PKCS12 container.
* During onboarding the app sends the raw PKCS12 data to Wear OS
  together with the container password. The connection is assumed to be
  encrypted and trusted so that no additional encryption is necessary.
@kleest
Copy link
Contributor Author

kleest commented Nov 26, 2023

Rebased on current master branch and removed the redundant try-catch block. Unfortunately I will not have time to add the expiry check within the next days. If @jpelgrom agrees, I will add the certificate expiry check in another PR later so that this PR is now complete.

@jpelgrom
Copy link
Member

If @jpelgrom agrees, I will add the certificate expiry check in another PR later so that this PR is now complete.

That's fine, no need to keep expanding the scope of the PR :)

Copy link
Member

@jpelgrom jpelgrom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your patience! I think this is good to merge now; no impact for regular users and builds on the existing implementation so minimal maintenance required.

@JBassett JBassett merged commit e0731c9 into home-assistant:master Dec 13, 2023
4 checks passed
@dynek
Copy link

dynek commented Apr 23, 2024

Has anyone successfully used this on a Xiaomi Watch ? With an ECC certificate I'm getting:
04-14 22:34:24.515 15199 15199 E SettingsWearViewModel: Watch was unable to register: java.io.IOException: exception decrypting data - java.security.NoSuchAlgorithmException: 1.2.840.113549.1.5.12 SecretKeyFactory not available
See: https://community.home-assistant.io/t/mtls-for-companion-app/717231

@virtualdj
Copy link

@dynek As another one replied to you on your linked thread, it seems that you're using an unsupported certificate on the watch (which connects on its own, so even if the same certificate works on the smartphone, it must work on the watch).

This type works on a Galaxy Watch 4:

# openssl x509 -in mycert.crt -text -noout | grep -i algo
        Signature Algorithm: sha256WithRSAEncryption
            Public Key Algorithm: rsaEncryption
    Signature Algorithm: sha256WithRSAEncryption

# openssl pkcs12 -in mycert.pfx -info -noout
Enter Import Password:
MAC: sha1, Iteration 2048
MAC length: 20, salt length: 8
PKCS7 Encrypted data: pbeWithSHA1And40BitRC2-CBC, Iteration 2048
Certificate bag
Certificate bag
PKCS7 Data
Shrouded Keybag: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2048

Of course you'll have to use the mycert.pfx file (which is created from the mycert.crt).

@dynek
Copy link

dynek commented Apr 23, 2024

OK so that certificate has been generated with the -legacy flag and that works 👍

@virtualdj
Copy link

@dynek Probably Wear OS had to strip something and chose the certificate algorithms, who knows? It's not very well documented.

@jeankhawand
Copy link

Hi @JBassett @kleest is it available on any pre-release builds ? I am unable to find PR reference in release note.
Thanks

@virtualdj
Copy link

I am unable to find PR reference in release note.

@jeankhawand It's already merged in the version avaliable on the Play Store, too. I'm using it successfully!

@jeankhawand
Copy link

jeankhawand commented Jul 1, 2024

I am unable to find PR reference in release note.

@jeankhawand It's already merged in the version avaliable on the Play Store, too. I'm using it successfully!

@virtualdj
thanks for your prompt response, well is there any flag to enable the TLS certificate section ? cause when I land to WearOS onboarding view I don't see it

@virtualdj
Copy link

virtualdj commented Jul 1, 2024

@jeankhawand It's not on the watch, but on the smartphone app (complementary app section, see the screenshots above).

@jeankhawand
Copy link

jeankhawand commented Jul 1, 2024

@jeankhawand It's not on the watch, but on the smartphone app (complementary app section, see the screenshots above).

@virtualdj
yes it's not on the watch on Campanion app, when I select my home assistant instance and move to login screen... I don't see the TLS certificate section. the only thing I can see is Device name prefilled. I am using latest build 2024.6.1-full Screenshot_2024-07-01-19-41-38-341_io.homeassistant.companion.android.jpg

@virtualdj
Copy link

virtualdj commented Jul 1, 2024

@jeankhawand Did you set the external URL in the smartphone app? It should point to a DNS of your proxy that provides the TLS Certificate Authentication.

Sorry I cannot be more precise because I configured it months ago (when it was released actually) and it worked so perfectly I didn't need to ever touch it 😁

EDIT: And you'll need the certificate installed on your smartphone, too, I think.

@jpelgrom
Copy link
Member

jpelgrom commented Jul 1, 2024

Going to lock this as a merged PR is not the place for troubleshooting. You'll need to have configured the server with mTLS on the phone, then it'll prompt you to select a certificate (again) when setting up the watch app. If you cannot get it to work, please create an issue.

@home-assistant home-assistant locked and limited conversation to collaborators Jul 1, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Wear OS: TLS Client Authentication Support does not work
8 participants