diff --git a/useragent/lib/features/connection/auth.dart b/useragent/lib/features/connection/auth.dart index cd512d7..8cd39ee 100644 --- a/useragent/lib/features/connection/auth.dart +++ b/useragent/lib/features/connection/auth.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:arbiter/features/connection/connection.dart'; import 'package:arbiter/features/connection/server_info_storage.dart'; import 'package:arbiter/features/identity/pk_manager.dart'; +import 'package:crypto/crypto.dart'; import 'package:arbiter/proto/arbiter.pbgrpc.dart'; import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth; import 'package:arbiter/proto/user_agent.pb.dart'; @@ -45,6 +46,18 @@ class ConnectionException implements Exception { String toString() => message; } +String certificateFingerprintHex(List derBytes) { + return sha256.convert(derBytes).toString(); +} + +bool isPinnedServerCertificate({ + required String expectedFingerprint, + required List certificateDer, +}) { + return certificateFingerprintHex(certificateDer) == + expectedFingerprint.toLowerCase(); +} + Future connectAndAuthorize( StoredServerInfo serverInfo, KeyHandle key, { @@ -155,7 +168,12 @@ Future _connect(StoredServerInfo serverInfo) async { connectTimeout: const Duration(seconds: 10), credentials: ChannelCredentials.secure( onBadCertificate: (cert, host) { - return true; + final isExpectedHost = host == serverInfo.address; + final isPinnedCert = isPinnedServerCertificate( + expectedFingerprint: serverInfo.caCertFingerprint, + certificateDer: cert.der, + ); + return isExpectedHost && isPinnedCert; }, ), ), diff --git a/useragent/test/features/connection/auth_tls_validation_test.dart b/useragent/test/features/connection/auth_tls_validation_test.dart new file mode 100644 index 0000000..f4eea5f --- /dev/null +++ b/useragent/test/features/connection/auth_tls_validation_test.dart @@ -0,0 +1,35 @@ +import 'package:arbiter/features/connection/auth.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('certificate pinning helpers', () { + test('certificateFingerprintHex returns SHA-256 in hex', () { + final fingerprint = certificateFingerprintHex('abc'.codeUnits); + + expect( + fingerprint, + 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', + ); + }); + + test('isPinnedServerCertificate matches expected fingerprint', () { + final matches = isPinnedServerCertificate( + expectedFingerprint: + 'BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD', + certificateDer: 'abc'.codeUnits, + ); + + expect(matches, isTrue); + }); + + test('isPinnedServerCertificate rejects mismatched fingerprint', () { + final matches = isPinnedServerCertificate( + expectedFingerprint: + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + certificateDer: 'abc'.codeUnits, + ); + + expect(matches, isFalse); + }); + }); +}