Skip to content

feat: add support for Android user-installed certificates #19830

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jfly
Copy link
Contributor

@jfly jfly commented Jul 9, 2025

Description

This is part of #15230.

Frustratingly, Dart/Flutter ignores user-installed certificates. Working around this requires rooting your Android device to install certificates as "system" certs, which isn't an option for everyone.

This is a known issue with Dart, see
dart-lang/sdk#50435 and
flutter/flutter#56607 for details.

I have read through
#15230 and #13555, and I understand that switching to
cronnet_http would also resolve this. While that may be the correct long-term approach, it looks like there are a lot of network codepaths in Immich, and as I know basically nothing about Dart, nor Flutter, nor Immich's codebase, I thought this would be a better short term approach.

This depends on my fork of
johnstef99/flutter_user_certificates_android, which I've sent a PR for here
johnstef99/flutter_user_certificates_android#2. If y'all don't like the supply chain implications of that, I'm happy to inline the implementation here instead.

How Has This Been Tested?

I have step-ca and DNS configured so that immich.mm resolves to an immich server with a certificate issued by my step-ca certificate authority. I've installed that certificate authority as a user-installed certificate on my Android phone.

  • With a fresh install of immich, I can configure https://immich.mm as a server
  • After logging in, I can view photos, upload photos and video, and play video.

Screenshots (if appropriate)

2025-07-08_18-06-21_pattern

The help text on this option is no longer true. I'd suggest that we deprecate it in favor of encouraging people to add certificates to their OS:

2025-07-08_17-59-03_pattern

Checklist:

  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation if applicable
  • I have no unrelated changes in the PR.
  • I have confirmed that any new dependencies are strictly necessary. Strictly? No. See my offer above about to inline the implementation if that's preferred.
  • I have written tests for new code (if applicable)
  • I have followed naming conventions/patterns in the surrounding code
  • All code in src/services/ uses repositories implementations for database calls, filesystem operations, etc.
  • All code in src/repositories/ is pretty basic/simple and does not have any immich specific logic (that belongs in src/services/)

@bo0tzz bo0tzz changed the title Add support for Android user-installed certificates feat: add support for Android user-installed certificates Jul 9, 2025
<base-config>
<trust-anchors>
<certificates src="system"/>
<certificates src="user"/>
Copy link
Member

Choose a reason for hiding this comment

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

There's no way to make this opt-in, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know, but everything I know about android development I learned in the last week while researching this issue.

Comment on lines 42 to 45
flutter_user_certificates_android:
git:
url: https://github.com/jfly/flutter_user_certificates_android.git
ref: "add-trusting"
Copy link
Member

Choose a reason for hiding this comment

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

If (big if, imo) we're keeping this repo reference then it should at least be pinned to a commit hash so we know what we're getting.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I totally understand. Happy to do any of the following:

  1. Pin to a specific commit, as you mention.
  2. Depend on the unpatched flutter_user_certificates_android and vendor just the changes from Add support for trusting android user certificates johnstef99/flutter_user_certificates_android#2.
  3. Vendor the whole thing. We're not talking about a lot of code here.

Just let me know which approach to take.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bo0tzz, friendly ping here. Which approach would you like me to take?

@jfly jfly force-pushed the support-android-user-certs branch 2 times, most recently from 8546274 to 01f0a57 Compare July 9, 2025 18:21
@shenlong-tanwen
Copy link
Member

@jfly Hey, Thanks a lot for working on this. However, if we are to merge this in, rather than deprecating the existing "Allow Self signed certificates" toggle, we could just remove all the custom verification logic all together. We don't need a lot of different codepath to address the same issue. However, this would mean that we would also need to test this exhaustively to make sure it does not break anything from the current setup. So things like:

  • mTLS
  • Self-signed certificates
  • Custom CAs

For all cases, we'd need to test both the dart side of things and the native side of things. Most network calls are from the dart side. Video player, Asset download are made from native side and we should validate these as well. If we are to remove the self signed toggle, we've to make sure everything works on iOS as well.

You could just inline all the changes. We are migrating to using pigeon for all message passing between flutter and the platform side, so you can add an API to fetch the certificates and load it in the dart's SecurityContext. We might also need some documentation change to inform users about installing certificates to the OS cert store, rather than loading it in the app like they used to.

The core team currently has got it's hands full with working towards the stable release. I know this is a significant undertaking for your free time, but if you can get this properly tested after removing the existing toggle and adding in your changes, we can happily merge this in. If you need help setting up specific test scenarios or access to different certificate setups, let us know - we can coordinate with the community to help test various configurations.

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
<base-config>
<base-config cleartextTrafficPermitted="true">

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why? Does my PR change the default behavior somehow? The docs say:

Any values that are not set use the platform default values.

Copy link
Member

Choose a reason for hiding this comment

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

The android:usesCleartextTraffic="true" set in the manifest file still seems to be respected. But it is best to also set it in the network config since the platform default varies between different versions of Android

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But it is best to also set it in the network config since the platform default varies between different versions of Android

That makes sense, but I don't see how this is related to my changes here. In the interest of keeping this PR targeted, should I send it a separate PR that adds this network_security_config.xml file with the explicit cleartextTrafficPermitted="true"?

@jfly
Copy link
Contributor Author

jfly commented Jul 21, 2025

I know this is a significant undertaking for your free time, but if you can get this properly tested after removing the existing toggle and adding in your changes, we can happily merge this in.

Sure, I'm happy to take this on. I've converted my understanding of your requirements into a checklist below. Please let me know if I'm missing anything:

  • Inline the changes (drop dependency on johnstef99/flutter_user_certificates_android)
    • Use pigeon for message passing between dart and kotlin.
  • Remove "Allow self-signed SSL certificates" checkbox
  • Docs? @shenlong-tanwen, please advise. IMO this is unnecessary, it's just fixing a bug and making things work the way users would expect. I do think a release note would be useful, is there a good place to add that?
  • Test on Android. Confirm you can view photos, upload photos and video, play video, and download assets.
  • Test on iOS. Confirm you can view photos, upload photos and video, play video, and download assets. @shenlong-tanwen, I will need help with this as I do not have access to an iOS device, nor development machine.
    • mTLS (aka "SSL Client Certificate")
    • Self-signed certificates
    • Custom CAs
  • Test upgrade migration from the older version with the setting on with a certificate to the new one without the toggle
    • Android
    • iOS

@shenlong-tanwen
Copy link
Member

Sure, I'm happy to take this on. I've converted my understanding of your requirements into a checklist below.

This is perfect! Thanks a lot for taking this on. As far the documentation, we can discuss this during the release of this PR. But yes, a callout in the release note might just do. It is also probably worth testing the migration from the older version with the setting on with a certificate to the new one without the toggle. Please feel free to ping me here or over discord if you need any help with the implementation or testing

@jfly jfly force-pushed the support-android-user-certs branch 2 times, most recently from 5182425 to c6419f1 Compare July 24, 2025 20:38
jfly added a commit to jfly/immich that referenced this pull request Jul 25, 2025
I ran into this while testing out
<immich-app#19830>. When I add, change,
or remove a client certificate under Immich's advanced settings, the
change wouldn't take effect until some mysterious point in the future.
For example:

1. Add a client certificate. It doesn't get used.
2. Remove certificate. *Now* the client certificate from step 1) is used.
3. Restart application. Now no client certificate is used.

This all boils down to some missing `await`s. The user would change the
cert, and we'd start asynchronously saving it to the store, and while
the save is still happening, [`HttpSSLOptions` pulls the "old" value out of
`SSLClientCertStoreVal`](https://github.com/immich-app/immich/blob/v1.136.0/mobile/lib/utils/http_ssl_options.dart#L30).

With the appropriate `await`s, this behaves much more sanely.
@jfly jfly force-pushed the support-android-user-certs branch from 0925f1e to 187a465 Compare July 25, 2025 01:28
jfly added a commit to jfly/immich that referenced this pull request Jul 25, 2025
I ran into this while testing out
<immich-app#19830>. When I add, change,
or remove a client certificate under Immich's advanced settings, the
change wouldn't take effect until some mysterious point in the future.
For example:

1. Add a client certificate. It doesn't get used.
2. Remove certificate. *Now* the client certificate from step 1) is used.
3. Restart application. Now no client certificate is used.

This all boils down to some missing `await`s. The user would change the
cert, and we'd start asynchronously saving it to the store, and while
the save is still happening, [`HttpSSLOptions` pulls the "old" value out of
`SSLClientCertStoreVal`](https://github.com/immich-app/immich/blob/v1.136.0/mobile/lib/utils/http_ssl_options.dart#L30).

With the appropriate `await`s, this behaves much more sanely.
@jfly jfly force-pushed the support-android-user-certs branch from 187a465 to 813adfc Compare July 25, 2025 01:30
jfly added a commit to jfly/immich that referenced this pull request Jul 25, 2025
I ran into this while testing out
<immich-app#19830>. When I add, change,
or remove a client certificate under Immich's advanced settings, the
change wouldn't take effect until some mysterious point in the future.
For example:

1. Add a client certificate. It doesn't get used.
2. Remove certificate. *Now* the client certificate from step 1) is used.
3. Restart application. Now no client certificate is used.

This all boils down to some missing `await`s. The user would change the
cert, and we'd start asynchronously saving it to the store, and while
the save is still happening, [`HttpSSLOptions` pulls the "old" value out of
`SSLClientCertStoreVal`](https://github.com/immich-app/immich/blob/v1.136.0/mobile/lib/utils/http_ssl_options.dart#L30).

With the appropriate `await`s, this behaves much more sanely.
jfly added a commit to jfly/immich that referenced this pull request Jul 25, 2025
I ran into this while testing out
<immich-app#19830>. When I add, change,
or remove a client certificate under Immich's advanced settings, the
change wouldn't take effect until some mysterious point in the future.
For example:

1. Add a client certificate. It doesn't get used.
2. Remove certificate. *Now* the client certificate from step 1) is used.
3. Restart application. Now no client certificate is used.

This all boils down to some missing `await`s. The user would change the
cert, and we'd start asynchronously saving it to the store, and while
the save is still happening, [`HttpSSLOptions` pulls the "old" value out of
`SSLClientCertStoreVal`](https://github.com/immich-app/immich/blob/v1.136.0/mobile/lib/utils/http_ssl_options.dart#L30).

With the appropriate `await`s, this behaves much more sanely.
alextran1502 pushed a commit that referenced this pull request Jul 25, 2025
I ran into this while testing out
<#19830>. When I add, change,
or remove a client certificate under Immich's advanced settings, the
change wouldn't take effect until some mysterious point in the future.
For example:

1. Add a client certificate. It doesn't get used.
2. Remove certificate. *Now* the client certificate from step 1) is used.
3. Restart application. Now no client certificate is used.

This all boils down to some missing `await`s. The user would change the
cert, and we'd start asynchronously saving it to the store, and while
the save is still happening, [`HttpSSLOptions` pulls the "old" value out of
`SSLClientCertStoreVal`](https://github.com/immich-app/immich/blob/v1.136.0/mobile/lib/utils/http_ssl_options.dart#L30).

With the appropriate `await`s, this behaves much more sanely.
@jfly jfly force-pushed the support-android-user-certs branch 2 times, most recently from 76f99a8 to 1b7af17 Compare July 25, 2025 05:36
This is part of immich-app#15230.

Frustratingly, Dart/Flutter ignores user-installed certificates. Working
around this requires rooting your Android device to install certificates
as "system" certs, which isn't an option for everyone.

This is a known issue with Dart, see
dart-lang/sdk#50435 and
flutter/flutter#56607 for details.

I have read through
<immich-app#15230> and
<immich-app#13555>, and I
understand that switching to
[`cronnet_http`](immich-app#14335 (comment))
would also resolve this. While that may be the correct long-term
approach, it looks like there are [a lot of network
codepaths](immich-app#15230 (comment))
in Immich, and as I know basically nothing about Dart, nor Flutter, nor
Immich's codebase, I thought this would be a better short term approach.

For the record: the important code here is copied from my fork of
`johnstef99/flutter_user_certificates_android`. See this PR for more
context: <johnstef99/flutter_user_certificates_android#2>.
@jfly jfly force-pushed the support-android-user-certs branch from 1b7af17 to db8d15e Compare July 25, 2025 06:05
This is unnecessary now that we support user-installed certificates on
Android. Users should be adding certs to their system instead, which
will work in more places (such as browsers), and won't blindly trust the
cert.
@jfly jfly force-pushed the support-android-user-certs branch from db8d15e to b396b38 Compare July 25, 2025 06:11
@jfly
Copy link
Contributor Author

jfly commented Jul 25, 2025

@shenlong-tanwen, I'm ready for you to take another look at this PR. I've updated my checklist with what I have done and not done.

Notably, with these changes, self-signed certificates work, but Android only lets you install them if they have the X.509v3 CA:true flag. You get a really confusing message saying a "private key is required to install a certificate". See https://android.stackexchange.com/questions/237141/how-to-get-android-11-to-trust-a-user-root-ca-without-a-private-key for details.

So, we need to decide what to do here. A couple proposals:

  1. Just make the change. Folks who want to self-sign their own certificates can do so, but they should be running a proper certificate authority. The existing feature (where we don't validate certs at all) is dangerous and should go away.
  2. Keep the "Allow self-signed SSL certificates" feature around, but reword it so it's clear that you should not be using it if you're running your own CA.
    • I still think it's a dangerous feature and should go away. Ideally we'd tell people it's deprecated and will be removed [at some point].

@richardlt
Copy link

Personally I agree with your second point, since this feature here will not strictly replace the existing flag it could be a breaking change for some users to remove it. I'm also not sure about the options for IOS users to install custom CA.
Maybe another proposal could be to keep it for now and to improve it later. For example in some applications if you try to reach a server with a self signed cert you have to approve it directly in the app and then this app will remember your choice so if the cert change you'll have to approve it again.

@jfly
Copy link
Contributor Author

jfly commented Jul 25, 2025

I'm also not sure about the options for IOS users to install custom CA.

This works fine. My wife is on iOS and has our custom CA installed on her phone, and immich works great.

For example in some applications if you try to reach a server with a self signed cert you have to approve it directly in the app and then this app will remember your choice so if the cert change you'll have to approve it again.

Yeah, that would be a ssh-style TOFU scheme. I don't have the same safety concerns, but that's a whole bunch of code that I don't really see the value in anyone writing, nor having the core immich team maintain.

IMO, it would be better for immich to take an opinionated stance and say that anyone running their immich server without "universally trusted" certs is required to be have that cert chained off of a root of trust with CA:true (aka: running their own certificate authority). Anyone who is doing this is likely going to run into the same issue with other self hosted services, anyways. Baking the trust into your OS is simplest for both the app developers and the users.

Of course I am happy to do whatever the core team needs to get this PR landed. IMO, the best solution would be to rebrand the existing option as dangerous and mark it deprecated, with the intent to remove it before the stable release.

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.

4 participants