Skip to content

WebView: validate SSL_UNTRUSTED certs via OkHttp trust store#6838

Open
cryptomilk wants to merge 1 commit into
home-assistant:mainfrom
cryptomilk:asn-wip
Open

WebView: validate SSL_UNTRUSTED certs via OkHttp trust store#6838
cryptomilk wants to merge 1 commit into
home-assistant:mainfrom
cryptomilk:asn-wip

Conversation

@cryptomilk
Copy link
Copy Markdown

@cryptomilk cryptomilk commented May 11, 2026

Summary

When connecting to a Home Assistant instance using a private CA, the WebView's Chromium TLS stack reports SSL_UNTRUSTED (error code 3) even though network_security_config.xml includes user CAs. The app's OkHttpClient correctly trusts user-installed CAs via AndroidCAStore, so we use it as a second opinion before rejecting the connection.

Observed log:
onReceivedSslError: primary error: 3 certificate: Issued to: CN=antares.milli.ways;
Issued by: CN=milly.ways CA Intermediate CA,O=milly.ways CA;
on URL: https://homeassistant.milli.ways/auth/authorize?...

TLSWebViewClient now overrides onReceivedSslError: for SSL_UNTRUSTED with a non-null URL it launches async validation via isTlsTrusted(); all other errors (IDMISMATCH, EXPIRED, etc.) are rejected immediately.

Checklist

  • New or updated tests have been added to cover the changes following the testing guidelines.
  • The code follows the project's code style and best_practices.
  • The changes have been thoroughly tested, and edge cases have been considered.
  • Changes are backward compatible whenever feasible. Any breaking changes are documented in the changelog for users and/or in the code for developers depending on the relevance.

Any other notes

I don't have a build environment for this. I already looked a the code last month and there was a similar PR, but I already thought it isn't going to fix it. I tested 2026.5.3 today. I still can't login. So this should hopefully fix it.

As this is SSL cert validation code it needs probably a few more eyes to check if it is correct. I now certificate stuff but not Android.

Claude helped reviewing the code and wrote the tests.

Copilot AI review requested due to automatic review settings May 11, 2026 19:53
@cryptomilk cryptomilk marked this pull request as draft May 11, 2026 19:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR changes the app’s WebView SSL error handling so that when Chromium reports SSL_UNTRUSTED, the app can asynchronously “double-check” trust using the app’s OkHttp TLS configuration (which includes user-installed CAs via AndroidCAStore) before rejecting the load. This is intended to unblock connecting to Home Assistant instances using private CAs that OkHttp trusts but WebView/Chromium rejects.

Changes:

  • Add async SSL_UNTRUSTED validation flow to TLSWebViewClient via an overridable isTlsTrusted(url) hook and a new onSslErrorRejected(...) callback.
  • Implement OkHttp-based trust validation in HAWebViewClient and thread lifecycle scopes through HAWebViewClientFactory.create(validationScope=...).
  • Add/adjust unit tests for SSL error handling and factory signature changes.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
app/src/main/kotlin/io/homeassistant/companion/android/util/TLSWebViewClient.kt Adds async handling for SSL_UNTRUSTED and introduces isTlsTrusted/onSslErrorRejected hooks
app/src/main/kotlin/io/homeassistant/companion/android/util/HAWebViewClient.kt Implements OkHttp-based TLS trust validation and updates factory to inject OkHttpClient + validation scope
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt Switches legacy activity to use onSslErrorRejected hook and passes a coroutine scope into TLSWebViewClient
app/src/main/kotlin/io/homeassistant/companion/android/onboarding/connection/ConnectionViewModel.kt Passes viewModelScope into factory for TLS validation lifecycle
app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt Passes viewModelScope into factory for TLS validation lifecycle
app/src/test/kotlin/io/homeassistant/companion/android/util/TLSWebViewClientSslErrorTest.kt New unit tests for async SSL_UNTRUSTED validation behavior in base client
app/src/test/kotlin/io/homeassistant/companion/android/util/HAWebViewClientTest.kt Updates SSL tests for new behavior and adds coverage for async acceptance path
app/src/test/kotlin/io/homeassistant/companion/android/onboarding/connection/ConnectionViewModelTest.kt Updates factory mocking for new validationScope parameter
app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt Updates factory mocking and argument indexes for new validationScope parameter

Comment thread app/src/main/kotlin/io/homeassistant/companion/android/util/HAWebViewClient.kt Outdated
Comment thread app/src/main/kotlin/io/homeassistant/companion/android/util/HAWebViewClient.kt Outdated
Comment thread app/src/main/kotlin/io/homeassistant/companion/android/util/HAWebViewClient.kt Outdated
Comment thread app/src/main/kotlin/io/homeassistant/companion/android/util/HAWebViewClient.kt Outdated
Comment thread app/src/main/kotlin/io/homeassistant/companion/android/util/HAWebViewClient.kt Outdated
Comment thread app/src/main/kotlin/io/homeassistant/companion/android/util/HAWebViewClient.kt Outdated
@cryptomilk
Copy link
Copy Markdown
Author

@TimoPtr thank you very much for the review! I will look into them.

@cryptomilk cryptomilk force-pushed the asn-wip branch 2 times, most recently from c34762a to 5763f9f Compare May 12, 2026 11:00
@cryptomilk cryptomilk marked this pull request as ready for review May 12, 2026 11:02
Comment thread app/src/main/kotlin/io/homeassistant/companion/android/util/TLSWebViewClient.kt Outdated
Comment thread app/src/main/kotlin/io/homeassistant/companion/android/util/TLSWebViewClient.kt Outdated
Comment thread app/src/main/kotlin/io/homeassistant/companion/android/util/TLSWebViewClient.kt Outdated
Comment thread app/src/main/kotlin/io/homeassistant/companion/android/util/TLSWebViewClient.kt Outdated
@TimoPtr TimoPtr marked this pull request as draft May 12, 2026 12:42
@TimoPtr TimoPtr marked this pull request as draft May 12, 2026 12:42
@cryptomilk cryptomilk marked this pull request as ready for review May 12, 2026 19:09
@cryptomilk
Copy link
Copy Markdown
Author

Tests passed locally.
Manually tested on my Fairphone 6 and I can connect to my HA instance.

@cryptomilk cryptomilk marked this pull request as draft May 12, 2026 20:00
@cryptomilk
Copy link
Copy Markdown
Author

Actually wait, I also tested on my old Zenfone 10 and it fails there. Investigating ...

@cryptomilk
Copy link
Copy Markdown
Author

The WebView auth page loads fine (you can see and approve the login), but the subsequent OkHttp POST to /auth/token in NameYourDeviceViewModel.onSaveClick() fails with "Trust anchor not found."

I don't understand why.

@cryptomilk
Copy link
Copy Markdown
Author

Ok, also works on the Zenfone 10. I guess it was some caching issue. I reinstalled and the setup worked just fine now.

I think we are good to go.

@cryptomilk cryptomilk marked this pull request as ready for review May 13, 2026 05:53
@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented May 13, 2026

Ok, also works on the Zenfone 10. I guess it was some caching issue. I reinstalled and the setup worked just fine now.

I think we are good to go.

Which APIs did you test in the end? Could you give us reproduction steps for us to test too?

@cryptomilk
Copy link
Copy Markdown
Author

Ok, also works on the Zenfone 10. I guess it was some caching issue. I reinstalled and the setup worked just fine now.
I think we are good to go.

Which APIs did you test in the end? Could you give us reproduction steps for us to test too?

Fairphone 6 with e/OS/: API level 35
Asus Zenfone 10: API level 34

@cryptomilk
Copy link
Copy Markdown
Author

cryptomilk commented May 13, 2026

I'm using https://smallstep.com/docs/step-ca/ for my CA Authority. I have a certificate for my home server which has Subject Alternative Names (SAN) for all the domain on it including home assistant.

You can also user OpenSSL as an alternative https://openssl-ca.readthedocs.io/en/latest/introduction.html

I have nginx as a proxy which does TLS and proxies to HA. It looks like this:

      '005-homeassistant.conf':
        enabled: True
        config:
          - server:
            - listen:
              - '0.0.0.0:443 ssl'
              - '[::]:443 ssl'
            - server_name: {{ ha_hostname }}.{{ grains.domain }}
            - location /:
              - proxy_pass: 'http://localhost:8123'
              - proxy_redirect: 'off'
              - proxy_http_version: '1.1'
              - proxy_set_header: Upgrade $http_upgrade
              - proxy_set_header: Connection "upgrade"
              - proxy_set_header: Host $host
              - proxy_set_header: X-Real-IP $remote_addr
              - proxy_set_header: X-Forwarded-For $proxy_add_x_forwarded_for
              - proxy_set_header: X-Forwarded-Proto $scheme

I downloaded the root certificate and installed it in Android.

Settings -> Security & Privacy -> Encryption & Credentials -> Install a certificate -> CA Certificate

Once it is installed the best is to reboot the phone. Then you can open HA Companion and enter the address manually to point to https://homeassistant.example.local

With the patch the certificate + intermediate chain will be checked and if valid the connection is accepted.

When connecting to a Home Assistant instance using a private CA, the
WebView's Chromium TLS stack reports SSL_UNTRUSTED (error code 3) even
though network_security_config.xml includes user CAs. The app's
OkHttpClient correctly trusts user-installed CAs via AndroidCAStore, so
we use it as a second opinion before rejecting the connection.

Observed log:
  onReceivedSslError: primary error: 3 certificate: Issued to: CN=antares.milli.ways;
  Issued by: CN=milly.ways CA Intermediate CA,O=milly.ways CA;
  on URL: https://homeassistant.milli.ways/auth/authorize?...

TLSWebViewClient now overrides onReceivedSslError: for SSL_UNTRUSTED
with a non-null URL it launches async validation via isTlsTrusted();
all other errors (IDMISMATCH, EXPIRED, etc.) are rejected immediately.

Claude has been used to review the changes and it has written the tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented May 18, 2026

I'm using smallstep.com/docs/step-ca for my CA Authority. I have a certificate for my home server which has Subject Alternative Names (SAN) for all the domain on it including home assistant.

You can also user OpenSSL as an alternative openssl-ca.readthedocs.io/en/latest/introduction.html

I have nginx as a proxy which does TLS and proxies to HA. It looks like this:

      '005-homeassistant.conf':
        enabled: True
        config:
          - server:
            - listen:
              - '0.0.0.0:443 ssl'
              - '[::]:443 ssl'
            - server_name: {{ ha_hostname }}.{{ grains.domain }}
            - location /:
              - proxy_pass: 'http://localhost:8123'
              - proxy_redirect: 'off'
              - proxy_http_version: '1.1'
              - proxy_set_header: Upgrade $http_upgrade
              - proxy_set_header: Connection "upgrade"
              - proxy_set_header: Host $host
              - proxy_set_header: X-Real-IP $remote_addr
              - proxy_set_header: X-Forwarded-For $proxy_add_x_forwarded_for
              - proxy_set_header: X-Forwarded-Proto $scheme

I downloaded the root certificate and installed it in Android.

Settings -> Security & Privacy -> Encryption & Credentials -> Install a certificate -> CA Certificate

Once it is installed the best is to reboot the phone. Then you can open HA Companion and enter the address manually to point to homeassistant.example.local

With the patch the certificate + intermediate chain will be checked and if valid the connection is accepted.

It's to setup mTLS no? I do use mTLS but I don't have the issue you are solving in this PR with my setup. Using Let's encrypt or Cloudflare.

We need a way to replicate the issue you have to validate it.

@cryptomilk
Copy link
Copy Markdown
Author

cryptomilk commented May 18, 2026

Your setup with Let's Encrypt or Cloudflare works out-of-the-box because those root CAs are already trusted by Android.

This PR: The client (Android app) validates the server's certificate, which is signed by a private CA that is not in Android's public trust store.

The fix uses OkHttp (which does honor user-installed CAs via AndroidCAStore) as a second validation path before rejecting the connection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants