dart-lang / http

A composable API for making HTTP requests in Dart.
https://pub.dev/packages/http
BSD 3-Clause "New" or "Revised" License
1.02k stars 354 forks source link

HttpClient does not send client certificate stored in SecurityContext on iOS #1277

Open yjiang-c opened 2 months ago

yjiang-c commented 2 months ago

Recently, I added mTLS client certificate function into Immich mobile app. The feature work fine on Android platform but does not work for iOS app.

The core change source code is an override of HttpOverrides in this file. The SecurityContext::usePrivateKeyBytes() is invoked to set client certificate. Based my log, the client certificate is set without exception.

But the following source code from this file for a http request failed.

  Future<bool> _isEndpointAvailable(String serverUrl) async {
    final Client client = Client();

    if (!serverUrl.endsWith('/api')) {
      serverUrl += '/api';
    }

    try {
      final response = await client
          .get(
            Uri.parse("$serverUrl/server-info/ping"),
            headers: getRequestHeaders(),
          )
          .timeout(const Duration(seconds: 5));

      _log.info("Pinging server with response code ${response.statusCode}");
      if (response.statusCode != 200) {
        _log.severe(
          "Server Gateway Error: ${response.body} - Cannot communicate to the server",
        );
        return false;
      }
    } on TimeoutException catch (_) {
      return false;
    } on SocketException catch (_) {
      return false;
    } catch (error, stackTrace) {
      _log.severe(
        "Error while checking server availability",
        error,
        stackTrace,
      );
      return false;
    }
    return true;
  }

The response from my Nginx server are shown as below to complain that Http Client does not send the client certificate. I trun Nginx debug logs and confirmed that Nginx did not get client certificate.

2024-07-28 01:04:26.037742 | SEVERE   | ApiService           | Server Gateway Error: <html>
<head><title>400 No required SSL certificate was sent</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx</center>
</body>
</html>
 - Cannot communicate to the server |

I tested with exact same server and client certificate, Immich Android app work fine but iOS 17.5.1 iPhone has this issue.

yjiang-c commented 2 months ago

Not sure whether it is related, NextCloud implemented mTLS client certificate on their iOS app recently. NextCloud iOS client was written with Apple native SDK but encountered the same issue. The issue is reported here. I did not see the same issue reported on NextCloud Android client.

yjiang-c commented 2 months ago

Just checked the C++ source code related to SecurityContext::useCertificateChainBytes() in security_context.cc and found it is finally invoke openssl API SSL_CTX_use_certificate in function UseChainBytesPKCS12. If iOS use the openssl as well, I cannot understand why SecurityContext::useCertificateChainBytes() is not needed to be invoked on iOS shown as below documentation in security_context.dart. I did not see SecurityContext::usePrivateKey() invoke openssl API SSL_CTX_use_certificate.

Could please confirm whether the following documentation is still true?


  /// iOS note: Only PKCS12 data is supported. It should contain both the private
  /// key and the certificate chain. On iOS one call to [usePrivateKey] with this
  /// data is used instead of two calls to [useCertificateChain] and
  /// [usePrivateKey].
yjiang-c commented 2 months ago

After some tests to call both SecurityContext::useCertificateChainBytes() and SecurityContext::usePrivateKey(), the issue is gone. The API documentation on both APIs related to iOS is wrong or deprecated. The API documentation needs to be updated.