fix(cert-manager): geschuetzte PFX nur noch als ruhiger Hinweis, wenn Zertifikat und Schluessel schon vorliegen
Tessera CI/CD / Lint & Type Check (push) Successful in 59s
Tessera CI/CD / Tests (push) Successful in 1m50s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 19s
Tessera CI/CD / Build & Publish Images (push) Successful in 4m48s

Die Aussteller-ZIP enthaelt neben .pem/.key eine PFX mit einem Passwort,
das niemand kennt (Windows certutil: ERROR_INVALID_PASSWORD bei leerem
Passwort). Die Uebersicht verlangte dafuer prominent ein Passwort, obwohl
Zertifikat und Schluessel einzeln vorliegen. Jetzt: neutraler Hinweis,
dass das Passwort nicht gebraucht wird, Passwortfeld nur auf Wunsch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-02 09:35:53 +02:00
parent f501ca6476
commit 90e2157613
4 changed files with 92 additions and 26 deletions
@@ -182,8 +182,12 @@ describe('OverviewTab', () => {
); );
}); });
it('geschuetzte PFX: Passwort eingeben und erneut pruefen', async () => { it('geschuetzte PFX ohne Schluessel im Paket: Passwort eingeben und erneut pruefen', async () => {
mockAnalyze.mockResolvedValueOnce({ items: [LEAF], locked: ['a.pfx'], ignored: [] }); mockAnalyze.mockResolvedValueOnce({
items: [{ ...LEAF, matchId: null }],
locked: ['a.pfx'],
ignored: [],
});
const pfx = new File(['x'], 'a.pfx'); const pfx = new File(['x'], 'a.pfx');
await upload([pfx]); await upload([pfx]);
@@ -194,6 +198,17 @@ describe('OverviewTab', () => {
expect(await screen.findAllByTestId('bundle-item')).toHaveLength(2); expect(await screen.findAllByTestId('bundle-item')).toHaveLength(2);
}); });
it('geschuetzte PFX, aber Zertifikat und Schluessel liegen schon vor: ruhiger Hinweis, Passwort optional', async () => {
mockAnalyze.mockResolvedValue({ items: [LEAF, INTER, KEY], locked: ['a.pfx'], ignored: [] });
await upload([new File(['x'], 'paket.zip')]);
const notice = await screen.findByTestId('locked-notice');
expect(within(notice).getByText(/lockedNotNeeded/)).toBeInTheDocument();
expect(within(notice).queryByLabelText('lockedPassword')).not.toBeInTheDocument();
fireEvent.click(within(notice).getByText('unlockAnyway'));
expect(within(notice).getByLabelText('lockedPassword')).toBeInTheDocument();
});
it('Fehler beim Pruefen wird angezeigt', async () => { it('Fehler beim Pruefen wird angezeigt', async () => {
mockAnalyze.mockRejectedValue(new Error('400')); mockAnalyze.mockRejectedValue(new Error('400'));
await upload([new File(['x'], 'a.txt')]); await upload([new File(['x'], 'a.txt')]);
@@ -54,6 +54,7 @@ export function OverviewTab() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<BundleAnalysis | null>(null); const [result, setResult] = useState<BundleAnalysis | null>(null);
const [unlockOpen, setUnlockOpen] = useState(false);
async function analyze(next: File[], pw: string) { async function analyze(next: File[], pw: string) {
if (next.length === 0) { if (next.length === 0) {
@@ -159,30 +160,20 @@ export function OverviewTab() {
{error && <p className="text-sm text-destructive">{error}</p>} {error && <p className="text-sm text-destructive">{error}</p>}
{result && result.locked.length > 0 && ( {result && result.locked.length > 0 && (
<div className="space-y-2 rounded-lg border border-status-warn/50 bg-status-warn/10 p-4"> <LockedNotice
<p className="text-sm text-foreground"> files={result.locked}
{t('locked', { files: result.locked.join(', ') })} // Liegen ein Zertifikat UND sein Schluessel schon einzeln vor,
</p> // steckt in der geschuetzten PFX nichts Neues (Aussteller-ZIP vom
<div className="flex flex-wrap items-center gap-2"> // 01.10.2026: PFX mit unbekanntem Passwort neben .pem/.key) — dann
<input // nur ein ruhiger Hinweis, das Passwort ist optional.
type="password" notNeeded={result.items.some((i) => i.kind === 'certificate' && i.matchId !== null)}
value={password} open={unlockOpen}
onChange={(e) => setPassword(e.target.value)} onOpen={() => setUnlockOpen(true)}
placeholder={t('lockedPassword')} password={password}
aria-label={t('lockedPassword')} onPassword={setPassword}
autoComplete="off" loading={loading}
className="w-64 rounded border border-border bg-background px-3 py-1.5 text-sm" onUnlock={() => void analyze(files, password)}
/> />
<button
type="button"
className="btn btn-secondary"
disabled={!password || loading}
onClick={() => void analyze(files, password)}
>
{t('unlock')}
</button>
</div>
</div>
)} )}
{result && result.items.length === 0 && !loading && ( {result && result.items.length === 0 && !loading && (
@@ -206,6 +197,62 @@ export function OverviewTab() {
); );
} }
function LockedNotice(props: {
files: string[];
notNeeded: boolean;
open: boolean;
onOpen: () => void;
password: string;
onPassword: (value: string) => void;
loading: boolean;
onUnlock: () => void;
}) {
const t = useTranslations('certManager.overview');
const showInput = !props.notNeeded || props.open;
return (
<div
className={`space-y-2 rounded-lg border p-4 ${
props.notNeeded ? 'border-border bg-muted/30' : 'border-status-warn/50 bg-status-warn/10'
}`}
data-testid="locked-notice"
>
<p className="text-sm text-foreground">
{t(props.notNeeded ? 'lockedNotNeeded' : 'locked', { files: props.files.join(', ') })}
</p>
{!showInput && (
<button
type="button"
onClick={props.onOpen}
className="text-xs text-muted-foreground underline"
>
{t('unlockAnyway')}
</button>
)}
{showInput && (
<div className="flex flex-wrap items-center gap-2">
<input
type="password"
value={props.password}
onChange={(e) => props.onPassword(e.target.value)}
placeholder={t('lockedPassword')}
aria-label={t('lockedPassword')}
autoComplete="off"
className="w-64 rounded border border-border bg-background px-3 py-1.5 text-sm"
/>
<button
type="button"
className="btn btn-secondary"
disabled={!props.password || props.loading}
onClick={props.onUnlock}
>
{t('unlock')}
</button>
</div>
)}
</div>
);
}
function BundleItemCard({ item, byId }: { item: BundleItem; byId: Record<string, BundleItem> }) { function BundleItemCard({ item, byId }: { item: BundleItem; byId: Record<string, BundleItem> }) {
const t = useTranslations('certManager.overview'); const t = useTranslations('certManager.overview');
const [pfxOpen, setPfxOpen] = useState(false); const [pfxOpen, setPfxOpen] = useState(false);
+2
View File
@@ -1239,8 +1239,10 @@
"analyzing": "Dateien werden geprüft …", "analyzing": "Dateien werden geprüft …",
"error": "Die Dateien konnten nicht geprüft werden. Bitte prüfen Sie, ob es Zertifikatsdateien sind.", "error": "Die Dateien konnten nicht geprüft werden. Bitte prüfen Sie, ob es Zertifikatsdateien sind.",
"locked": "Geschützt: {files}. Geben Sie das Passwort ein, um auch diesen Inhalt zu lesen.", "locked": "Geschützt: {files}. Geben Sie das Passwort ein, um auch diesen Inhalt zu lesen.",
"lockedNotNeeded": "{files} ist mit einem Passwort geschützt. Sie brauchen es nicht: Zertifikat und privater Schlüssel liegen schon als einzelne Dateien vor und sind unten aufgeführt. Eine neue PFX-Datei mit eigenem Passwort können Sie beim Serverzertifikat erstellen.",
"lockedPassword": "Passwort der geschützten Datei", "lockedPassword": "Passwort der geschützten Datei",
"unlock": "Entsperren", "unlock": "Entsperren",
"unlockAnyway": "Trotzdem mit Passwort öffnen",
"nothingFound": "In den Dateien wurde kein Zertifikat, Schlüssel und keine Zertifikatsanfrage gefunden.", "nothingFound": "In den Dateien wurde kein Zertifikat, Schlüssel und keine Zertifikatsanfrage gefunden.",
"ignored": "Nicht verwendet (kein Zertifikat erkannt): {files}", "ignored": "Nicht verwendet (kein Zertifikat erkannt): {files}",
"type": { "type": {
+2
View File
@@ -1239,8 +1239,10 @@
"analyzing": "Checking files …", "analyzing": "Checking files …",
"error": "The files could not be checked. Please make sure they are certificate files.", "error": "The files could not be checked. Please make sure they are certificate files.",
"locked": "Protected: {files}. Enter the password to read this content as well.", "locked": "Protected: {files}. Enter the password to read this content as well.",
"lockedNotNeeded": "{files} is password-protected. You do not need it: certificate and private key are already available as separate files and listed below. You can create a new PFX file with your own password at the server certificate.",
"lockedPassword": "Password of the protected file", "lockedPassword": "Password of the protected file",
"unlock": "Unlock", "unlock": "Unlock",
"unlockAnyway": "Open with password anyway",
"nothingFound": "No certificate, key or certificate request was found in the files.", "nothingFound": "No certificate, key or certificate request was found in the files.",
"ignored": "Not used (no certificate detected): {files}", "ignored": "Not used (no certificate detected): {files}",
"type": { "type": {