diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md index 276a68eeb..59c030fbd 100644 --- a/dio/CHANGELOG.md +++ b/dio/CHANGELOG.md @@ -11,6 +11,28 @@ See the [Migration Guide][] for the complete breaking changes list.** On web, timeout handling is best-effort because synchronous JavaScript work cannot be preempted. - Fix `FormData.clone()` dropping `boundaryName` and `camelCaseContentDisposition`, so a retried multipart request now keeps the original options instead of silently falling back to the defaults. - Fix `QueuedInterceptor` stalling its queue forever when the active request is cancelled while its callback is still pending (never calls `next`/`resolve`/`reject`), which blocked every subsequent request routed through the interceptor. +- **Security:** `IOHttpClientAdapter.validateCertificate` now fires between + the TLS handshake and the first HTTP byte for direct HTTPS connections + (no proxy, no custom `createHttpClient`), making it suitable for true + certificate or public-key pinning (issue #2418). Previously, an + attacker presenting a publicly trusted certificate for the wrong host + could receive the full request body and headers before the callback + aborted. +- The callback also continues to run **after the response head arrives** + as defense in depth — both invocations receive the same leaf + certificate, so for fingerprint-style pinning the second call is + idempotent. Callbacks with side effects (logging, metrics) will + observe one extra call per request on the direct-HTTPS path. +- HTTPS routed through a proxy continues to use the post-response + validation path only (HttpClient performs its own `CONNECT` tunnel and + TLS handshake, bypassing the pre-emission hook). For pre-emission + pinning behind a proxy, supply `createHttpClient` and configure + `HttpClient.connectionFactory` yourself. +- On the pre-emission path, `validateCertificate` is the sole gate for + certificate trust — system / CA validation is bypassed (matching what + users already configure via `badCertificateCallback: (_, _, _) => true` + in the existing pinning example), so self-signed and pinned-CA setups + work without supplying `createHttpClient`. ## 5.9.2 diff --git a/dio/lib/src/adapters/io_adapter.dart b/dio/lib/src/adapters/io_adapter.dart index e7255caae..9f5d49dcb 100644 --- a/dio/lib/src/adapters/io_adapter.dart +++ b/dio/lib/src/adapters/io_adapter.dart @@ -25,6 +25,29 @@ typedef ValidateCertificate = bool Function( int port, ); +/// Internal sentinel thrown from [IOHttpClientAdapter]'s connection factory +/// when [IOHttpClientAdapter.validateCertificate] rejects the peer certificate. +/// [_fetch] catches it and rethrows as [DioException.badCertificate]. +class _BadCertificateException implements HandshakeException { + _BadCertificateException(this.host, this.port, this.cert); + + final String host; + final int port; + final X509Certificate? cert; + + @override + String get type => 'HandshakeException'; + + @override + String get message => 'validateCertificate returned false'; + + @override + OSError? get osError => null; + + @override + String toString() => 'HandshakeException: $message (host=$host, port=$port)'; +} + /// Creates an [IOHttpClientAdapter]. HttpClientAdapter createAdapter() => IOHttpClientAdapter(); @@ -44,14 +67,53 @@ class IOHttpClientAdapter implements HttpClientAdapter { /// When this callback is set, [Dio] will call it every /// time it needs a [HttpClient]. + /// + /// Note: supplying this disables the pre-emission TLS hook used by + /// [validateCertificate]. See [validateCertificate] for details. CreateHttpClient? createHttpClient; - /// Allows the user to decide if the response certificate is good. - /// If this function is missing, then the certificate is allowed. - /// This method is called only if both the [SecurityContext] and - /// [badCertificateCallback] accept the certificate chain. Those - /// methods evaluate the root or intermediate certificate, while - /// [validateCertificate] evaluates the leaf certificate. + /// Allows the user to decide if the leaf certificate of the TLS connection + /// is good. If this function is missing, then the certificate is allowed. + /// + /// For **direct HTTPS** connections (no proxy) with the default + /// [createHttpClient], this callback fires **before** the request body + /// is sent, immediately after the TLS handshake completes. Returning + /// `false` aborts the connection without leaking any request data and + /// surfaces as [DioException.badCertificate]. This makes the callback + /// suitable for certificate or public-key pinning. + /// + /// The callback also runs **after the response head arrives** for the + /// same connection. The two invocations receive the same leaf + /// certificate, so for fingerprint-style pinning the second call is + /// idempotent. If your callback has side effects (logging, metrics), + /// expect to see them twice per request on the direct-HTTPS path. + /// + /// **HTTPS through a proxy:** `HttpClient` performs its own `CONNECT` + /// tunnel and TLS handshake, so the pre-emission hook is bypassed. + /// Validation runs only post-response on this path — the request body + /// has already been transmitted by the time the callback is invoked. + /// For pre-emission pinning behind a proxy, use [createHttpClient] and + /// install your own `connectionFactory` on the returned client. + /// + /// **Custom [createHttpClient]:** dio cannot install the pre-emission + /// hook on a user-built [HttpClient]; the callback runs post-response + /// (the legacy 5.x behavior). For pre-emission validation in that case, + /// set `connectionFactory` on the [HttpClient] you return. + /// + /// On the pre-emission path, this callback is the **sole** gate for + /// certificate trust: system / CA validation is bypassed (the equivalent + /// of `HttpClient.badCertificateCallback: (_, _, _) => true` in the + /// existing example), so the callback receives every leaf cert and is + /// solely responsible for accepting or rejecting it. This matches what + /// users already configure manually for pinning and lets self-signed and + /// pinned-CA setups work without supplying [createHttpClient]. If you + /// do not intend to perform pinning, do not set this callback — + /// supplying any non-null value disables stdlib chain validation. + /// + /// Any [SecurityContext] you set on a custom [HttpClient] is **not** + /// used by the pre-emission path. To combine mTLS or a custom trust store + /// with pinning, supply a [createHttpClient] and pin in the post-response + /// path. ValidateCertificate? validateCertificate; HttpClient? _cachedHttpClient; @@ -110,6 +172,11 @@ class IOHttpClientAdapter implements HttpClientAdapter { ); } }); + } on _BadCertificateException catch (e) { + throw DioException.badCertificate( + requestOptions: options, + error: e.cert, + ); } on SocketException catch (e) { if (e.message.contains('timed out')) { final Duration effectiveTimeout; @@ -190,6 +257,18 @@ class IOHttpClientAdapter implements HttpClientAdapter { } if (validateCertificate != null) { + // Post-response validation runs unconditionally as defense in depth. + // On the pre-emission path it is redundant — the same cert was + // already approved by the [HttpClient.connectionFactory] hook — but + // it is the only line of defense for paths where pre-emission did + // not run: (a) when the user supplied a custom [createHttpClient], + // (b) when [validateCertificate] was set after the [HttpClient] was + // first cached, or (c) for HTTPS through a proxy (see the doc-comment + // on [validateCertificate]). For pinning the callback is idempotent + // so the redundancy is harmless; callbacks with side effects will + // observe one extra call per request on the direct-HTTPS path. + // On plain HTTP, [responseStream.certificate] is null and the user + // typically rejects nulls (matching the example). final host = options.uri.host; final port = options.uri.port; final bool isCertApproved = validateCertificate!( @@ -259,7 +338,72 @@ class IOHttpClientAdapter implements HttpClientAdapter { return createHttpClient!(); } final client = HttpClient()..idleTimeout = const Duration(seconds: 3); + if (validateCertificate != null) { + client.connectionFactory = _pinnedConnectionFactory; + } // ignore: deprecated_member_use, deprecated_member_use_from_same_package return onHttpClientCreate?.call(client) ?? client; } + + /// Custom connection factory that performs the TLS handshake and runs + /// [validateCertificate] *before* yielding the socket to [HttpClient], + /// so a rejected certificate cannot leak request bytes. + /// + /// For HTTPS requests routed through a proxy, this factory cannot + /// pre-emption-validate: HttpClient writes its own `CONNECT` and performs + /// its own TLS handshake on top of the socket we return. In that case the + /// proxy branch returns a plain socket to the proxy and validation falls + /// back to the post-response check in [_fetch]. + Future> _pinnedConnectionFactory( + Uri url, + String? proxyHost, + int? proxyPort, + ) async { + // Proxy: hand HttpClient a plain socket to the proxy and let it own the + // CONNECT-tunnel and TLS upgrade. The post-response block in [_fetch] + // performs validation in this path. + if (proxyHost != null) { + return Socket.startConnect(proxyHost, proxyPort!); + } + + // Plain HTTP, no proxy: hand HttpClient the target socket. + if (url.scheme != 'https') { + return Socket.startConnect(url.host, url.port); + } + + // Snapshot at call time so a runtime mutation of the public field + // is honored on every new connection. + final validator = validateCertificate; + + // [SecureSocket.startConnect] returns a stock SDK [ConnectionTask] — + // available since well before dio's min SDK of 2.18 — so we don't + // need [ConnectionTask.fromSocket] (Dart 3.5+) and don't need to + // subclass/vendor [ConnectionTask] (which is `final class` from Dart + // 3.0). We await `task.socket` once here to do the leaf-cert check, + // then return the same task; [HttpClient] re-awaits `task.socket` + // and gets the already-resolved [SecureSocket] immediately. + // + // Covariance: [ConnectionTask] has no bound on its type parameter, + // so [ConnectionTask] is implicitly assignable to + // [ConnectionTask] at the return position. + final task = await SecureSocket.startConnect( + url.host, + url.port, + supportedProtocols: const ['http/1.1'], + // When [validateCertificate] is set, defer all certificate + // validation to the user's callback. This makes self-signed and + // pinned-CA setups work without forcing the user to also supply + // [createHttpClient]. + onBadCertificate: validator == null ? null : (_) => true, + ); + if (validator != null) { + final ss = await task.socket; + final cert = ss.peerCertificate; + if (!validator(cert, url.host, url.port)) { + ss.destroy(); + throw _BadCertificateException(url.host, url.port, cert); + } + } + return task; + } } diff --git a/dio/test/pinning_test.dart b/dio/test/pinning_test.dart index 97b785d5c..bd9ba6977 100644 --- a/dio/test/pinning_test.dart +++ b/dio/test/pinning_test.dart @@ -1,5 +1,8 @@ @TestOn('vm') +import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:dio/dio.dart'; @@ -21,6 +24,49 @@ void main() { } group('pinning:', () { + // A throwaway self-signed localhost keypair is generated at runtime so + // that no private key is committed to the repository. openssl is already + // a requirement for scripts/prepare_pinning_certs.sh (used by the tls + // network tests in this group), so it is a safe build dependency to lean + // on here. + late Directory certDir; + late String certPath; + late String keyPath; + + setUpAll(() { + certDir = Directory.systemTemp.createTempSync('dio_pinning_test_'); + certPath = '${certDir.path}/server_cert.pem'; + keyPath = '${certDir.path}/server_key.pem'; + final result = Process.runSync('openssl', [ + 'req', + '-x509', + '-newkey', + 'rsa:2048', + '-keyout', + keyPath, + '-out', + certPath, + '-days', + '3650', + '-nodes', + '-subj', + '/CN=localhost', + ]); + if (result.exitCode != 0) { + throw StateError( + 'Failed to generate a self-signed test certificate with openssl ' + '(exit ${result.exitCode}). openssl must be installed to run the ' + 'pinning tests.\n${result.stderr}', + ); + } + }); + + tearDownAll(() { + if (certDir.existsSync()) { + certDir.deleteSync(recursive: true); + } + }); + test('trusted host allowed with no approver', () async { await Dio().get(trustedCertUrl); }); @@ -151,31 +197,361 @@ void main() { tags: ['tls'], ); - test( - '2 requests == 2 approvals', - () async { - int approvalCount = 0; + group('pre-emission validation:', () { + late HttpServer secureServer; + late int bytesReceived; + late int requestsHandled; + + setUp(() async { + bytesReceived = 0; + requestsHandled = 0; + final ctx = SecurityContext() + ..useCertificateChain(certPath) + ..usePrivateKey(keyPath); + secureServer = await HttpServer.bindSecure('localhost', 0, ctx); + secureServer.listen((req) async { + requestsHandled++; + try { + await for (final chunk in req) { + bytesReceived += chunk.length; + } + } finally { + req.response.statusCode = 200; + req.response.write('ok'); + await req.response.close(); + } + }); + }); + + tearDown(() async { + await secureServer.close(force: true); + }); + + test('rejection blocks request body emission', () async { + // Load-bearing regression test for #2418: when validateCertificate + // returns false, the request body must never reach the server. + final dio = Dio(); + dio.httpClientAdapter = IOHttpClientAdapter( + validateCertificate: (cert, host, port) => false, + ); + DioException? error; + try { + await dio.post( + 'https://localhost:${secureServer.port}/leak', + data: 'SENSITIVE-PAYLOAD', + ); + fail('did not throw'); + } on DioException catch (e) { + error = e; + } + // Allow any in-flight server activity to settle. + await Future.delayed(const Duration(milliseconds: 50)); + expect(error, isNotNull); + expect(error.type, DioExceptionType.badCertificate); + expect(bytesReceived, 0); + expect(requestsHandled, 0); + }); + + test('approval lets the request through', () async { + bool approverCalled = false; final dio = Dio(); - // badCertificateCallback never called for trusted certificate dio.httpClientAdapter = IOHttpClientAdapter( validateCertificate: (cert, host, port) { - approvalCount++; - return fingerprint() == sha256.convert(cert!.der).toString(); + approverCalled = true; + return true; }, ); - Response response = await dio.get( - trustedCertUrl, - options: Options(validateStatus: (status) => true), + final res = await dio.post( + 'https://localhost:${secureServer.port}/echo', + data: 'hi', + options: Options(responseType: ResponseType.plain), ); - expect(response.data, isNotNull); - response = await dio.get( - trustedCertUrl, - options: Options(validateStatus: (status) => true), + expect(approverCalled, isTrue); + expect(res.statusCode, 200); + expect(res.data, 'ok'); + expect(requestsHandled, 1); + }); + + test('createHttpClient escape hatch keeps post-response validation', + () async { + // When createHttpClient is supplied, the connectionFactory cannot + // be installed without clobbering the user-built HttpClient. + // validateCertificate continues to fire post-response (legacy 5.x + // behavior). + final dio = Dio(); + dio.httpClientAdapter = IOHttpClientAdapter( + createHttpClient: () => + HttpClient()..badCertificateCallback = (cert, host, port) => true, + validateCertificate: (cert, host, port) => false, + ); + DioException? error; + try { + await dio.post( + 'https://localhost:${secureServer.port}/legacy', + data: 'payload', + ); + fail('did not throw'); + } on DioException catch (e) { + error = e; + } + expect(error, isNotNull); + expect(error.type, DioExceptionType.badCertificate); + // Legacy path: server received the request before validation ran. + expect(requestsHandled, 1); + expect(bytesReceived, greaterThan(0)); + }); + + test('two pooled requests: 1 pre-emission + 2 post-response validations', + () async { + // Two back-to-back requests within idleTimeout (3s) share one pooled + // TCP connection, so the connectionFactory pre-emission hook runs + // once (per connection) while the post-response block runs once per + // request. The validator is therefore invoked 3 times for 2 requests. + // This replaces the former network-dependent '2 requests' test with a + // deterministic local-server assertion. + int approvals = 0; + final dio = Dio(); + dio.httpClientAdapter = IOHttpClientAdapter( + validateCertificate: (cert, host, port) { + approvals++; + return true; + }, + ); + await dio.post( + 'https://localhost:${secureServer.port}/one', + data: 'a', + options: Options(responseType: ResponseType.plain), + ); + await dio.post( + 'https://localhost:${secureServer.port}/two', + data: 'b', + options: Options(responseType: ResponseType.plain), + ); + expect(approvals, 3); + expect(requestsHandled, 2); + }); + }); + + test('plain http skips pre-emission validation', () async { + // The pre-emission factory short-circuits non-https URLs to + // [Socket.startConnect] without invoking the validator. The + // post-response block still fires (with a null cert, matching 5.9.x + // semantics), so the user's callback decides what to do with null — + // the typical pinning callback rejects it and the request fails. + final server = await HttpServer.bind('localhost', 0); + try { + server.listen((req) { + req.response.statusCode = 200; + req.response.write('plain'); + req.response.close(); + }); + X509Certificate? observedCert; + final dio = Dio(); + dio.httpClientAdapter = IOHttpClientAdapter( + validateCertificate: (cert, host, port) { + observedCert = cert; + return true; // accept the null cert so we can assert observability + }, + ); + final res = await dio.get( + 'http://localhost:${server.port}/', + options: Options(responseType: ResponseType.plain), + ); + expect(res.statusCode, 200); + expect(res.data, 'plain'); + // Only the post-response call fires (and with null cert because + // there is no TLS handshake on plain HTTP). + expect(observedCert, isNull); + } finally { + await server.close(force: true); + } + }); + + test( + 'validateCertificate set after construction still fires (legacy ' + 'post-response path)', () async { + // Late-mutation regression guard: a user who constructs the adapter + // without validateCertificate and sets it later should still have the + // callback fire (via the legacy post-response path), even though the + // pre-emission factory could not be installed. + final ctx = SecurityContext() + ..useCertificateChain(certPath) + ..usePrivateKey(keyPath); + final server = await HttpServer.bindSecure('localhost', 0, ctx); + server.listen((req) { + req.response.statusCode = 200; + req.response.write('ok'); + req.response.close(); + }); + try { + final adapter = IOHttpClientAdapter( + // ignore: deprecated_member_use_from_same_package + onHttpClientCreate: (client) => + client..badCertificateCallback = (cert, host, port) => true, + ); + final dio = Dio()..httpClientAdapter = adapter; + // Warm the cached HttpClient with no validator set. + final ok = await dio.get( + 'https://localhost:${server.port}/', + options: Options(responseType: ResponseType.plain), + ); + expect(ok.statusCode, 200); + // Now set the validator. + adapter.validateCertificate = (cert, host, port) => false; + await expectLater( + dio.get('https://localhost:${server.port}/'), + throwsA( + allOf([ + isA(), + (DioException e) => e.type == DioExceptionType.badCertificate, + ]), + ), + ); + } finally { + await server.close(force: true); + } + }); + + group('through a CONNECT proxy (post-response validation):', () { + late HttpServer secureBackend; + late ServerSocket proxy; + late _StubProxy proxyState; + + setUp(() async { + final ctx = SecurityContext() + ..useCertificateChain(certPath) + ..usePrivateKey(keyPath); + secureBackend = await HttpServer.bindSecure('localhost', 0, ctx); + secureBackend.listen((req) async { + req.response.statusCode = 200; + req.response.write('ok'); + await req.response.close(); + }); + proxyState = _StubProxy(); + proxy = await proxyState.bind('localhost'); + }); + + tearDown(() async { + await proxy.close(); + await secureBackend.close(force: true); + }); + + test('validateCertificate runs (post-response) through proxy', () async { + bool validatorCalled = false; + final adapter = IOHttpClientAdapter( + validateCertificate: (cert, host, port) { + validatorCalled = true; + expect(host, 'localhost'); + expect(port, secureBackend.port); + return true; + }, + // ignore: deprecated_member_use_from_same_package + onHttpClientCreate: (client) { + client.badCertificateCallback = (cert, host, port) => true; + client.findProxy = (uri) => 'PROXY localhost:${proxy.port}'; + return client; + }, + ); + final dio = Dio()..httpClientAdapter = adapter; + final res = await dio.get( + 'https://localhost:${secureBackend.port}/', + options: Options(responseType: ResponseType.plain), + ); + expect(validatorCalled, isTrue); + expect(res.statusCode, 200); + expect(res.data, 'ok'); + expect(proxyState.connectCount, 1); + }); + + test( + 'rejection through proxy raises badCertificate ' + '(post-response, not pre-emission)', () async { + final adapter = IOHttpClientAdapter( + validateCertificate: (cert, host, port) => false, + // ignore: deprecated_member_use_from_same_package + onHttpClientCreate: (client) { + client.badCertificateCallback = (cert, host, port) => true; + client.findProxy = (uri) => 'PROXY localhost:${proxy.port}'; + return client; + }, + ); + final dio = Dio()..httpClientAdapter = adapter; + await expectLater( + dio.post( + 'https://localhost:${secureBackend.port}/leak', + data: 'SENSITIVE-PAYLOAD', + ), + throwsA( + allOf([ + isA(), + (DioException e) => e.type == DioExceptionType.badCertificate, + ]), + ), + ); + // The proxy was reached, the tunnel was established, the request + // was forwarded to the backend (post-response path leaks the body + // — documented limitation of the proxy path). + expect(proxyState.connectCount, 1); + }); + }); + }); +} + +/// Stub HTTP/1.1 CONNECT proxy for tests. Accepts `CONNECT host:port` and +/// opens a TCP tunnel to the target, forwarding bytes both directions. +class _StubProxy { + int connectCount = 0; + + Future bind(String address) async { + final server = await ServerSocket.bind(address, 0); + server.listen(_handleClient); + return server; + } + + void _handleClient(Socket client) { + final headerBuffer = BytesBuilder(copy: false); + Socket? backend; + bool tunneled = false; + + client.listen( + (chunk) async { + if (tunneled) { + backend?.add(chunk); + return; + } + headerBuffer.add(chunk); + final decoded = + ascii.decode(headerBuffer.toBytes(), allowInvalid: true); + final end = decoded.indexOf('\r\n\r\n'); + if (end < 0) { + return; + } + final requestLine = decoded.substring(0, decoded.indexOf('\r\n')); + final match = RegExp(r'^CONNECT (\S+):(\d+)').firstMatch(requestLine); + if (match == null) { + client.write('HTTP/1.1 400 Bad Request\r\n\r\n'); + await client.close(); + return; + } + connectCount++; + try { + backend = + await Socket.connect(match.group(1)!, int.parse(match.group(2)!)); + } catch (_) { + client.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); + await client.close(); + return; + } + client.write('HTTP/1.1 200 Connection established\r\n\r\n'); + tunneled = true; + backend!.listen( + client.add, + onError: (_) => client.destroy(), + onDone: () => client.destroy(), ); - expect(response.data, isNotNull); - expect(approvalCount, 2); }, - tags: ['tls'], + onError: (_) => backend?.destroy(), + onDone: () => backend?.destroy(), ); - }); + } } diff --git a/dio/test/stacktrace_test.dart b/dio/test/stacktrace_test.dart index b6bc559b8..7612b8ea4 100644 --- a/dio/test/stacktrace_test.dart +++ b/dio/test/stacktrace_test.dart @@ -182,38 +182,38 @@ void main() async { test( DioExceptionType.badCertificate, () async { - await HttpOverrides.runWithHttpOverrides( - () async { - final dio = Dio() - ..options.baseUrl = 'https://does.not.exist' - ..httpClientAdapter = IOHttpClientAdapter( - validateCertificate: (_, __, ___) => false, - ); - - when(httpClientMock.openUrl('GET', any)).thenAnswer( - (_) async { - final request = MockHttpClientRequest(); - final response = MockHttpClientResponse(); - when(request.close()).thenAnswer((_) => Future.value(response)); - when(response.certificate).thenReturn(null); - return request; - }, - ); + // Exercise the legacy post-response validation path by supplying + // [createHttpClient]; the connectionFactory hook is intentionally + // skipped in that case (see [IOHttpClientAdapter.validateCertificate] + // and #2418), so the mock HttpClient need only stub [openUrl] / + // [request.close] / [response.certificate]. + final dio = Dio() + ..options.baseUrl = 'https://does.not.exist' + ..httpClientAdapter = IOHttpClientAdapter( + createHttpClient: () => httpClientMock, + validateCertificate: (cert, host, port) => false, + ); - await expectLater( - dio.get('/test'), - throwsA( - allOf([ - isA(), - (DioException e) => e.type == DioExceptionType.badCertificate, - (DioException e) => e.stackTrace - .toString() - .contains('test/stacktrace_test.dart'), - ]), - ), - ); + when(httpClientMock.openUrl('GET', any)).thenAnswer( + (_) async { + final request = MockHttpClientRequest(); + final response = MockHttpClientResponse(); + when(request.close()).thenAnswer((_) => Future.value(response)); + when(response.certificate).thenReturn(null); + return request; }, - MockHttpOverrides(), + ); + + await expectLater( + dio.get('/test'), + throwsA( + allOf([ + isA(), + (DioException e) => e.type == DioExceptionType.badCertificate, + (DioException e) => + e.stackTrace.toString().contains('test/stacktrace_test.dart'), + ]), + ), ); }, testOn: '!browser', diff --git a/example_dart/lib/certificate_pinning.dart b/example_dart/lib/certificate_pinning.dart index 08ee374fc..8221aa1ec 100644 --- a/example_dart/lib/certificate_pinning.dart +++ b/example_dart/lib/certificate_pinning.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:crypto/crypto.dart'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; @@ -16,19 +14,13 @@ void main() async { // should look like this: 'ee5ce1dfa7a53657c545c62b65802e4272878dabd65c0aadcf85783ebb0b4d5c'; - // Don't trust any certificate just because their root cert is trusted + // As of dio 5.10.0, validateCertificate fires before the request body is + // sent (when createHttpClient is not supplied), so a rejected certificate + // blocks the request rather than just the response. dio.httpClientAdapter = IOHttpClientAdapter( - createHttpClient: () { - final client = HttpClient( - context: SecurityContext(withTrustedRoots: false), - ); - // You can test the intermediate / root cert here. We just ignore it. - client.badCertificateCallback = (cert, host, port) => true; - return client; - }, validateCertificate: (cert, host, port) { - // Check that the cert fingerprint matches the one we expect - // We definitely require _some_ certificate + // Check that the cert fingerprint matches the one we expect. + // We definitely require _some_ certificate. if (cert == null) { return false; }