security(useragent): validate server cert fingerprint and host instead of accepting all certificates
Some checks failed
ci/woodpecker/pr/useragent-analyze Pipeline failed

This commit is contained in:
CleverWild
2026-04-10 14:44:16 +02:00
parent 62dff3f810
commit 9e1ab51398
2 changed files with 54 additions and 1 deletions

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:arbiter/features/connection/connection.dart'; import 'package:arbiter/features/connection/connection.dart';
import 'package:arbiter/features/connection/server_info_storage.dart'; import 'package:arbiter/features/connection/server_info_storage.dart';
import 'package:arbiter/features/identity/pk_manager.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/arbiter.pbgrpc.dart';
import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth; import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth;
import 'package:arbiter/proto/user_agent.pb.dart'; import 'package:arbiter/proto/user_agent.pb.dart';
@@ -45,6 +46,18 @@ class ConnectionException implements Exception {
String toString() => message; String toString() => message;
} }
String certificateFingerprintHex(List<int> derBytes) {
return sha256.convert(derBytes).toString();
}
bool isPinnedServerCertificate({
required String expectedFingerprint,
required List<int> certificateDer,
}) {
return certificateFingerprintHex(certificateDer) ==
expectedFingerprint.toLowerCase();
}
Future<Connection> connectAndAuthorize( Future<Connection> connectAndAuthorize(
StoredServerInfo serverInfo, StoredServerInfo serverInfo,
KeyHandle key, { KeyHandle key, {
@@ -155,7 +168,12 @@ Future<Connection> _connect(StoredServerInfo serverInfo) async {
connectTimeout: const Duration(seconds: 10), connectTimeout: const Duration(seconds: 10),
credentials: ChannelCredentials.secure( credentials: ChannelCredentials.secure(
onBadCertificate: (cert, host) { onBadCertificate: (cert, host) {
return true; final isExpectedHost = host == serverInfo.address;
final isPinnedCert = isPinnedServerCertificate(
expectedFingerprint: serverInfo.caCertFingerprint,
certificateDer: cert.der,
);
return isExpectedHost && isPinnedCert;
}, },
), ),
), ),

View File

@@ -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);
});
});
}