fix(store,doctor): doc-verifier findings on the trust surfaces
Some checks failed
Security / Security check (push) Failing after 1s

The doc-verifier pass over the new trust/exposure surfaces came back
PASS with five improvements, all applied:
- the store policy notice gains a 'Learn more' into the security doc
  (the notice named security.require_signatures but not where it
  lives)
- 'blocked' disables the trust gate's install button — an active
  button contradicted the 'install would be refused' statement right
  above it (guard test added)
- the unknown-exposure tooltip now says what the operator can do
  (check where the name resolves)
- dead l10n key verifPillUnverified removed (unverified is the
  page-level notice, never a card pill)
- stale header comment in install_verification.dart corrected

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-21 13:23:58 +02:00
parent 2f076ccf29
commit bef2dbe988
9 changed files with 34 additions and 22 deletions

View file

@ -2,8 +2,9 @@
// per-entry statement of how an install would be verified under
// the CURRENT policy (computed hub-side with the same resolvers
// the install gate enforces). One mapping, used by the trust-gate
// dialog, the module detail sheet and the store card, so every
// surface tells the same story.
// dialog and the store card's blocked pill, so every surface
// tells the same story. (The module detail sheet routes installs
// through the dialog and needs no copy of its own.)
import '../l10n/app_localizations.dart';

View file

@ -1194,7 +1194,7 @@
"svcExposurePublic": "öffentlich erreichbar",
"svcExposureUnknown": "Reichweite unbekannt",
"svcExposurePublicHint": "Dieser Dienst-Endpunkt liegt auf einer öffentlichen Adresse — prüfen Sie, ob er abgesichert ist (TLS, Auth, Firewall).",
"svcExposureUnknownHint": "Hostname statt IP-Adresse — der Hub löst zur Einstufung bewusst keine Namen auf.",
"svcExposureUnknownHint": "Hostname statt IP-Adresse — der Hub löst zur Einstufung bewusst keine Namen auf. Prüfen Sie selbst, wohin der Name zeigt.",
"doctorLinkStore": "Store öffnen",
"doctorLinkApprovals": "Freigaben öffnen",
"doctorLinkAudit": "Protokoll öffnen",
@ -1629,7 +1629,6 @@
"installConfirmTrustTitle": "Vertrauen & Sicherheit",
"installConfirmTrustBody": "Das Modul läuft in einer Sandbox: Es darf nur auf die Netzwerk-Endpunkte, Dateien und Umgebungsvariablen zugreifen, die es selbst deklariert — der Hub setzt diese Liste durch. Die vollständige Berechtigungsliste sehen Sie nach der Installation in den Modul-Details.",
"storePolicyUnverifiedNotice": "Die Signaturpflicht ist in der Hub-Richtlinie ausgeschaltet — Installationen werden nicht kryptografisch geprüft. Der Installations-Dialog zeigt den Status je Modul; für geprüfte Installationen security.require_signatures aktivieren.",
"verifPillUnverified": "unsigniert",
"verifPillBlocked": "blockiert",
"verifPinnedKey": "Signatur wird geprüft — hinterlegter Store-Schlüssel",
"verifPinnedKeyBody": "Der Hub verifiziert das Paket beim Installieren gegen den fest hinterlegten Schlüssel dieser Quelle. Ein manipuliertes Paket wird abgelehnt.",

View file

@ -1218,7 +1218,7 @@
"svcExposurePublic": "publicly reachable",
"svcExposureUnknown": "reach unknown",
"svcExposurePublicHint": "This service endpoint sits on a public address — make sure it is protected (TLS, auth, firewall).",
"svcExposureUnknownHint": "Hostname instead of an IP address — the hub deliberately does not resolve names to classify.",
"svcExposureUnknownHint": "Hostname instead of an IP address — the hub deliberately does not resolve names to classify. Check yourself where the name resolves.",
"doctorLinkStore": "Open the store",
"doctorLinkApprovals": "Open approvals",
"doctorLinkAudit": "Open the audit log",
@ -1653,7 +1653,6 @@
"installConfirmTrustTitle": "Trust & security",
"installConfirmTrustBody": "The module runs in a sandbox: it may only touch the network endpoints, files, and environment variables it declares itself — the hub enforces that list. The full permission list is visible in the module details after installation.",
"storePolicyUnverifiedNotice": "Signature enforcement is switched off in the hub policy — installs are not cryptographically verified. The install dialog shows the per-module status; enable security.require_signatures for verified installs.",
"verifPillUnverified": "unsigned",
"verifPillBlocked": "blocked",
"verifPinnedKey": "Signature checked — pinned store key",
"verifPinnedKeyBody": "The hub verifies the bundle at install time against this source's pinned key. A tampered bundle is refused.",

View file

@ -3725,7 +3725,7 @@ abstract class AppLocalizations {
/// No description provided for @svcExposureUnknownHint.
///
/// In en, this message translates to:
/// **'Hostname instead of an IP address — the hub deliberately does not resolve names to classify.'**
/// **'Hostname instead of an IP address — the hub deliberately does not resolve names to classify. Check yourself where the name resolves.'**
String get svcExposureUnknownHint;
/// No description provided for @doctorLinkStore.
@ -4940,12 +4940,6 @@ abstract class AppLocalizations {
/// **'Signature enforcement is switched off in the hub policy — installs are not cryptographically verified. The install dialog shows the per-module status; enable security.require_signatures for verified installs.'**
String get storePolicyUnverifiedNotice;
/// No description provided for @verifPillUnverified.
///
/// In en, this message translates to:
/// **'unsigned'**
String get verifPillUnverified;
/// No description provided for @verifPillBlocked.
///
/// In en, this message translates to:

View file

@ -2167,7 +2167,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get svcExposureUnknownHint =>
'Hostname statt IP-Adresse — der Hub löst zur Einstufung bewusst keine Namen auf.';
'Hostname statt IP-Adresse — der Hub löst zur Einstufung bewusst keine Namen auf. Prüfen Sie selbst, wohin der Name zeigt.';
@override
String get doctorLinkStore => 'Store öffnen';
@ -2910,9 +2910,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get storePolicyUnverifiedNotice =>
'Die Signaturpflicht ist in der Hub-Richtlinie ausgeschaltet — Installationen werden nicht kryptografisch geprüft. Der Installations-Dialog zeigt den Status je Modul; für geprüfte Installationen security.require_signatures aktivieren.';
@override
String get verifPillUnverified => 'unsigniert';
@override
String get verifPillBlocked => 'blockiert';

View file

@ -2171,7 +2171,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get svcExposureUnknownHint =>
'Hostname instead of an IP address — the hub deliberately does not resolve names to classify.';
'Hostname instead of an IP address — the hub deliberately does not resolve names to classify. Check yourself where the name resolves.';
@override
String get doctorLinkStore => 'Open the store';
@ -2906,9 +2906,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get storePolicyUnverifiedNotice =>
'Signature enforcement is switched off in the hub policy — installs are not cryptographically verified. The install dialog shows the per-module status; enable security.require_signatures for verified installs.';
@override
String get verifPillUnverified => 'unsigned';
@override
String get verifPillBlocked => 'blocked';

View file

@ -15,6 +15,7 @@ import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import '../data/error_presentation.dart';
import '../data/hub.dart';
import '../data/install_verification.dart';
import 'welcome.dart' show showFaiDoc;
import '../data/system_actions.dart';
import '../data/today_story_loader.dart';
import '../l10n/app_localizations.dart';
@ -399,6 +400,12 @@ class _StorePageState extends State<StorePage> {
child: ChainInlineHelp(
icon: Icons.gpp_maybe_outlined,
text: l.storePolicyUnverifiedNotice,
// One click to the security doc that
// explains WHERE the key lives
// (doc-verifier finding).
onLearnMore: () =>
showFaiDoc(context, 'security'),
learnMoreLabel: l.buttonLearnMore,
),
),
if (_aiThinking ||

View file

@ -258,7 +258,13 @@ class ChainInstallConfirmDialog extends StatelessWidget {
child: Text(l.buttonCancel),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
// "blocked" means the hub WOULD refuse this install (policy
// demands signatures, no key material for this source)
// offering an active Install button would contradict the
// statement right above it (doc-verifier finding).
onPressed: item.installVerification == 'blocked'
? null
: () => Navigator.pop(context, true),
child: Text(l.buttonInstall),
),
],

View file

@ -105,6 +105,18 @@ void main() {
);
});
testWidgets('blocked disables the install button', (tester) async {
await tester.pumpWidget(
_host(ChainInstallConfirmDialog(item: _item(verification: 'blocked'))),
);
await tester.pumpAndSettle();
// The statement says the hub would refuse the button must
// not contradict it.
expect(find.text('Installation würde abgelehnt'), findsOneWidget);
final button = tester.widget<FilledButton>(find.byType(FilledButton));
expect(button.onPressed, isNull);
});
testWidgets('trust gate keeps the generic note for old hubs', (tester) async {
await tester.pumpWidget(_host(ChainInstallConfirmDialog(item: _item())));
await tester.pumpAndSettle();