diff --git a/helper/helper/piv.py b/helper/helper/piv.py index 34e7dffd6..6b3408651 100644 --- a/helper/helper/piv.py +++ b/helper/helper/piv.py @@ -262,26 +262,6 @@ def reset(self): def slots(self): return SlotsNode(self.session) - @action(closes_child=False) - def examine_file(self, data: bytes, password: str | None = None): - try: - private_key, certs = _parse_file(data, password) - certificate = _choose_cert(certs) - - return dict( - status=True, - password=password is not None, - key_type=( - KEY_TYPE.from_public_key(private_key.public_key()) - if private_key - else None - ), - cert_info=_get_cert_info(certificate), - ) - except InvalidPasswordError: - logger.debug("Invalid or missing password", exc_info=True) - return dict(status=False) - @action(closes_child=False) def validate_rfc4514(self, data: str): try: @@ -459,6 +439,40 @@ def move_key( self._refresh() return dict() + @action + def examine_file(self, data: bytes, password: str | None = None): + try: + private_key, certs = _parse_file(data, password) + certificate = _choose_cert(certs) + + response = dict( + status=True, + password=password is not None, + key_type=( + KEY_TYPE.from_public_key(private_key.public_key()) + if private_key + else None + ), + cert_info=_get_cert_info(certificate), + ) + + if self.metadata and certificate and not private_key: + # Verify that the public key of a cert matches the + # private key in the slot + slot_public_key = self.metadata.public_key + cert_public_key = certificate.public_key() + public_key_match = slot_public_key.public_bytes( + encoding=Encoding.DER, format=PublicFormat.SubjectPublicKeyInfo + ) == cert_public_key.public_bytes( + encoding=Encoding.DER, format=PublicFormat.SubjectPublicKeyInfo + ) + response["public_key_match"] = public_key_match + + return response + except InvalidPasswordError: + logger.debug("Invalid or missing password", exc_info=True) + return dict(status=False) + @action def import_file(self, data: bytes, password: str | None = None, **kwargs): try: diff --git a/lib/desktop/piv/state.dart b/lib/desktop/piv/state.dart index 4fea78555..954a0ebe8 100644 --- a/lib/desktop/piv/state.dart +++ b/lib/desktop/piv/state.dart @@ -436,8 +436,12 @@ class _DesktopPivSlotsNotifier extends PivSlotsNotifier { } @override - Future examine(String data, {String? password}) async { - final result = await _session.command('examine_file', params: { + Future examine(SlotId slot, String data, + {String? password}) async { + final result = await _session.command('examine_file', target: [ + 'slots', + slot.hexId + ], params: { 'data': data, 'password': password, }); diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index ecd8c783f..10683fe4d 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -705,6 +705,12 @@ "slot": {} } }, + "p_warning_public_key_mismatch": null, + "@p_warning_public_key_mismatch": { + "placeholders": { + "slot": {} + } + }, "l_key_moved": "Schlüssel verschoben", "l_key_and_certificate_moved": "Schlüssel und Zertifikat verschoben", "p_subject_desc": "Distinguished Name (DN) RFC 4514 konform formatiert.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 75707678e..97e72f171 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -705,6 +705,12 @@ "slot": {} } }, + "p_warning_public_key_mismatch": "The public key of the certificate does not match the private key in slot {slot}.", + "@p_warning_public_key_mismatch": { + "placeholders": { + "slot": {} + } + }, "l_key_moved": "Key moved", "l_key_and_certificate_moved": "Key and certificate moved", "p_subject_desc": "A distinguished name (DN) formatted in accordance to the RFC 4514 specification.", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 280df3e50..b302499ba 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -705,6 +705,12 @@ "slot": {} } }, + "p_warning_public_key_mismatch": null, + "@p_warning_public_key_mismatch": { + "placeholders": { + "slot": {} + } + }, "l_key_moved": "Clé déplacée", "l_key_and_certificate_moved": "Clé et certificat déplacés", "p_subject_desc": "DN (nom distinctif) formaté conformément à la spécification RFC 4514.", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index dffef0c93..37e082899 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -705,6 +705,12 @@ "slot": {} } }, + "p_warning_public_key_mismatch": null, + "@p_warning_public_key_mismatch": { + "placeholders": { + "slot": {} + } + }, "l_key_moved": "キーを移動しました", "l_key_and_certificate_moved": "キーと証明書が移動されました", "p_subject_desc": "RFC 4514仕様に準拠した形式の識別名(DN)。", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 176928a3e..da88575fc 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -705,6 +705,12 @@ "slot": {} } }, + "p_warning_public_key_mismatch": null, + "@p_warning_public_key_mismatch": { + "placeholders": { + "slot": {} + } + }, "l_key_moved": "Klucz został przeniesiony", "l_key_and_certificate_moved": "Klucz i certyfikat został przeniesiony", "p_subject_desc": "Nazwa wyróżniająca (DN) sformatowana zgodnie ze specyfikacją RFC 4514.", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 3e5850ab4..7bf93494f 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -705,6 +705,12 @@ "slot": {} } }, + "p_warning_public_key_mismatch": null, + "@p_warning_public_key_mismatch": { + "placeholders": { + "slot": {} + } + }, "l_key_moved": "Kľúč bol presunutý", "l_key_and_certificate_moved": "Kľúč a certifikát boli presunuté", "p_subject_desc": "Rozlišujúci názov (DN) naformátovaný v súlade so špecifikáciou RFC 4514.", diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index de9dfd4c3..c09fa1560 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -705,6 +705,12 @@ "slot": {} } }, + "p_warning_public_key_mismatch": null, + "@p_warning_public_key_mismatch": { + "placeholders": { + "slot": {} + } + }, "l_key_moved": "Khóa đã được di chuyển", "l_key_and_certificate_moved": "Khóa và chứng chỉ đã được di chuyển", "p_subject_desc": "Tên phân biệt (DN) được định dạng theo tiêu chuẩn RFC 4514.", diff --git a/lib/piv/models.dart b/lib/piv/models.dart index 47caf237b..6eb46ea9e 100644 --- a/lib/piv/models.dart +++ b/lib/piv/models.dart @@ -319,6 +319,7 @@ class PivExamineResult with _$PivExamineResult { required bool password, required KeyType? keyType, required CertInfo? certInfo, + bool? publicKeyMatch, }) = _ExamineResult; factory PivExamineResult.invalidPassword() = _InvalidPassword; diff --git a/lib/piv/models.freezed.dart b/lib/piv/models.freezed.dart index 0d9688692..4360ac196 100644 --- a/lib/piv/models.freezed.dart +++ b/lib/piv/models.freezed.dart @@ -2365,22 +2365,24 @@ PivExamineResult _$PivExamineResultFromJson(Map json) { mixin _$PivExamineResult { @optionalTypeArgs TResult when({ - required TResult Function( - bool password, KeyType? keyType, CertInfo? certInfo) + required TResult Function(bool password, KeyType? keyType, + CertInfo? certInfo, bool? publicKeyMatch) result, required TResult Function() invalidPassword, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ - TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo)? + TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo, + bool? publicKeyMatch)? result, TResult? Function()? invalidPassword, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ - TResult Function(bool password, KeyType? keyType, CertInfo? certInfo)? + TResult Function(bool password, KeyType? keyType, CertInfo? certInfo, + bool? publicKeyMatch)? result, TResult Function()? invalidPassword, required TResult orElse(), @@ -2437,7 +2439,11 @@ abstract class _$$ExamineResultImplCopyWith<$Res> { _$ExamineResultImpl value, $Res Function(_$ExamineResultImpl) then) = __$$ExamineResultImplCopyWithImpl<$Res>; @useResult - $Res call({bool password, KeyType? keyType, CertInfo? certInfo}); + $Res call( + {bool password, + KeyType? keyType, + CertInfo? certInfo, + bool? publicKeyMatch}); $CertInfoCopyWith<$Res>? get certInfo; } @@ -2458,6 +2464,7 @@ class __$$ExamineResultImplCopyWithImpl<$Res> Object? password = null, Object? keyType = freezed, Object? certInfo = freezed, + Object? publicKeyMatch = freezed, }) { return _then(_$ExamineResultImpl( password: null == password @@ -2472,6 +2479,10 @@ class __$$ExamineResultImplCopyWithImpl<$Res> ? _value.certInfo : certInfo // ignore: cast_nullable_to_non_nullable as CertInfo?, + publicKeyMatch: freezed == publicKeyMatch + ? _value.publicKeyMatch + : publicKeyMatch // ignore: cast_nullable_to_non_nullable + as bool?, )); } @@ -2497,6 +2508,7 @@ class _$ExamineResultImpl implements _ExamineResult { {required this.password, required this.keyType, required this.certInfo, + this.publicKeyMatch, final String? $type}) : $type = $type ?? 'result'; @@ -2509,13 +2521,15 @@ class _$ExamineResultImpl implements _ExamineResult { final KeyType? keyType; @override final CertInfo? certInfo; + @override + final bool? publicKeyMatch; @JsonKey(name: 'runtimeType') final String $type; @override String toString() { - return 'PivExamineResult.result(password: $password, keyType: $keyType, certInfo: $certInfo)'; + return 'PivExamineResult.result(password: $password, keyType: $keyType, certInfo: $certInfo, publicKeyMatch: $publicKeyMatch)'; } @override @@ -2527,12 +2541,15 @@ class _$ExamineResultImpl implements _ExamineResult { other.password == password) && (identical(other.keyType, keyType) || other.keyType == keyType) && (identical(other.certInfo, certInfo) || - other.certInfo == certInfo)); + other.certInfo == certInfo) && + (identical(other.publicKeyMatch, publicKeyMatch) || + other.publicKeyMatch == publicKeyMatch)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, password, keyType, certInfo); + int get hashCode => + Object.hash(runtimeType, password, keyType, certInfo, publicKeyMatch); /// Create a copy of PivExamineResult /// with the given fields replaced by the non-null parameter values. @@ -2545,34 +2562,36 @@ class _$ExamineResultImpl implements _ExamineResult { @override @optionalTypeArgs TResult when({ - required TResult Function( - bool password, KeyType? keyType, CertInfo? certInfo) + required TResult Function(bool password, KeyType? keyType, + CertInfo? certInfo, bool? publicKeyMatch) result, required TResult Function() invalidPassword, }) { - return result(password, keyType, certInfo); + return result(password, keyType, certInfo, publicKeyMatch); } @override @optionalTypeArgs TResult? whenOrNull({ - TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo)? + TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo, + bool? publicKeyMatch)? result, TResult? Function()? invalidPassword, }) { - return result?.call(password, keyType, certInfo); + return result?.call(password, keyType, certInfo, publicKeyMatch); } @override @optionalTypeArgs TResult maybeWhen({ - TResult Function(bool password, KeyType? keyType, CertInfo? certInfo)? + TResult Function(bool password, KeyType? keyType, CertInfo? certInfo, + bool? publicKeyMatch)? result, TResult Function()? invalidPassword, required TResult orElse(), }) { if (result != null) { - return result(password, keyType, certInfo); + return result(password, keyType, certInfo, publicKeyMatch); } return orElse(); } @@ -2620,7 +2639,8 @@ abstract class _ExamineResult implements PivExamineResult { factory _ExamineResult( {required final bool password, required final KeyType? keyType, - required final CertInfo? certInfo}) = _$ExamineResultImpl; + required final CertInfo? certInfo, + final bool? publicKeyMatch}) = _$ExamineResultImpl; factory _ExamineResult.fromJson(Map json) = _$ExamineResultImpl.fromJson; @@ -2628,6 +2648,7 @@ abstract class _ExamineResult implements PivExamineResult { bool get password; KeyType? get keyType; CertInfo? get certInfo; + bool? get publicKeyMatch; /// Create a copy of PivExamineResult /// with the given fields replaced by the non-null parameter values. @@ -2685,8 +2706,8 @@ class _$InvalidPasswordImpl implements _InvalidPassword { @override @optionalTypeArgs TResult when({ - required TResult Function( - bool password, KeyType? keyType, CertInfo? certInfo) + required TResult Function(bool password, KeyType? keyType, + CertInfo? certInfo, bool? publicKeyMatch) result, required TResult Function() invalidPassword, }) { @@ -2696,7 +2717,8 @@ class _$InvalidPasswordImpl implements _InvalidPassword { @override @optionalTypeArgs TResult? whenOrNull({ - TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo)? + TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo, + bool? publicKeyMatch)? result, TResult? Function()? invalidPassword, }) { @@ -2706,7 +2728,8 @@ class _$InvalidPasswordImpl implements _InvalidPassword { @override @optionalTypeArgs TResult maybeWhen({ - TResult Function(bool password, KeyType? keyType, CertInfo? certInfo)? + TResult Function(bool password, KeyType? keyType, CertInfo? certInfo, + bool? publicKeyMatch)? result, TResult Function()? invalidPassword, required TResult orElse(), diff --git a/lib/piv/models.g.dart b/lib/piv/models.g.dart index f32beaa38..485de1da3 100644 --- a/lib/piv/models.g.dart +++ b/lib/piv/models.g.dart @@ -209,6 +209,7 @@ _$ExamineResultImpl _$$ExamineResultImplFromJson(Map json) => certInfo: json['cert_info'] == null ? null : CertInfo.fromJson(json['cert_info'] as Map), + publicKeyMatch: json['public_key_match'] as bool?, $type: json['runtimeType'] as String?, ); @@ -217,6 +218,7 @@ Map _$$ExamineResultImplToJson(_$ExamineResultImpl instance) => 'password': instance.password, 'key_type': _$KeyTypeEnumMap[instance.keyType], 'cert_info': instance.certInfo, + 'public_key_match': instance.publicKeyMatch, 'runtimeType': instance.$type, }; diff --git a/lib/piv/state.dart b/lib/piv/state.dart index 1ba4ca54d..82485192f 100644 --- a/lib/piv/state.dart +++ b/lib/piv/state.dart @@ -48,7 +48,8 @@ final pivSlotsProvider = AsyncNotifierProvider.autoDispose abstract class PivSlotsNotifier extends AutoDisposeFamilyAsyncNotifier, DevicePath> { - Future examine(String data, {String? password}); + Future examine(SlotId slot, String data, + {String? password}); Future validateRfc4514(String value); Future<(SlotMetadata?, String?)> read(SlotId slot); Future generate( diff --git a/lib/piv/views/import_file_dialog.dart b/lib/piv/views/import_file_dialog.dart index ddd003749..50456e5c1 100644 --- a/lib/piv/views/import_file_dialog.dart +++ b/lib/piv/views/import_file_dialog.dart @@ -78,7 +78,9 @@ class _ImportFileDialogState extends ConsumerState { }); final result = await ref .read(pivSlotsProvider(widget.devicePath).notifier) - .examine(_data, password: _password.isNotEmpty ? _password : null); + .examine(widget.pivSlot.slot, _data, + password: _password.isNotEmpty ? _password : null); + setState(() { _state = result; _passwordIsWrong = result.maybeWhen( @@ -176,7 +178,7 @@ class _ImportFileDialogState extends ConsumerState { ), ), ), - result: (_, keyType, certInfo) { + result: (_, keyType, certInfo, publicKeyMatch) { final isFips = ref.watch(currentDeviceDataProvider).valueOrNull?.info.isFips ?? false; @@ -311,6 +313,33 @@ class _ImportFileDialogState extends ConsumerState { ), ], ), + if (publicKeyMatch == false) + Container( + decoration: BoxDecoration( + color: colorScheme.tertiary, + borderRadius: BorderRadius.circular(4.0), + ), + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Icon( + Symbols.warning_amber, + fill: 1, + size: 16, + color: colorScheme.onTertiary, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + l10n.p_warning_public_key_mismatch( + widget.pivSlot.slot.getDisplayName(l10n)), + style: textTheme.bodySmall + ?.copyWith(color: colorScheme.onTertiary), + ), + ), + ], + ), + ), SizedBox( height: 140, // Needed for layout, adapt if text sizes changes