feat(reality): warn when target cert chain is too small for ML-DSA-65 (#6470)

* feat(reality): warn when target cert chain is too small for ML-DSA-65

Expose peer cert-chain DER size from the REALITY scanner and surface a UI
warning when ML-DSA-65 is enabled but the chain is under xray-core's 3500-byte
minimum, so silent fallback failures are easier to catch.

Fixes #5973

* fix(reality): gate scanner ML-DSA tag and sync docs OpenAPI

Only warn on short cert chains in the target scanner when ML-DSA-65 is
enabled. Copy frontend/public/openapi.json to docs/public/openapi.json
and fix oxfmt wrapping in the new test.

---------

Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com>
This commit is contained in:
mrchatam
2026-09-12 12:43:45 +03:30
committed by GitHub
parent 5fbd2b490c
commit b467d4c676
23 changed files with 221 additions and 5 deletions
+8
View File
@@ -3710,6 +3710,11 @@
"example": "h2", "example": "h2",
"type": "string" "type": "string"
}, },
"certChainBytes": {
"description": "CertChainBytes is the sum of DER lengths of the presented peer chain.\nxray-core ML-DSA-65 REALITY needs >= 3500 bytes (constant lives in xray-core).",
"example": 3427,
"type": "integer"
},
"certChainValid": { "certChainValid": {
"description": "CertChainValid ignores the name: a trusted chain presented for other names\nstill has serverNames the panel can offer instead of the failing SNI.", "description": "CertChainValid ignores the name: a trusted chain presented for other names\nstill has serverNames the panel can offer instead of the failing SNI.",
"example": true, "example": true,
@@ -3792,6 +3797,7 @@
}, },
"required": [ "required": [
"alpn", "alpn",
"certChainBytes",
"certChainValid", "certChainValid",
"certIssuer", "certIssuer",
"certSubject", "certSubject",
@@ -7240,6 +7246,7 @@
"success": true, "success": true,
"obj": { "obj": {
"alpn": "h2", "alpn": "h2",
"certChainBytes": 3427,
"certChainValid": true, "certChainValid": true,
"certIssuer": "Google Trust Services", "certIssuer": "Google Trust Services",
"certSubject": "cloudflare.com", "certSubject": "cloudflare.com",
@@ -7319,6 +7326,7 @@
"obj": [ "obj": [
{ {
"alpn": "h2", "alpn": "h2",
"certChainBytes": 3427,
"certChainValid": true, "certChainValid": true,
"certIssuer": "Google Trust Services", "certIssuer": "Google Trust Services",
"certSubject": "cloudflare.com", "certSubject": "cloudflare.com",
+8
View File
@@ -3710,6 +3710,11 @@
"example": "h2", "example": "h2",
"type": "string" "type": "string"
}, },
"certChainBytes": {
"description": "CertChainBytes is the sum of DER lengths of the presented peer chain.\nxray-core ML-DSA-65 REALITY needs >= 3500 bytes (constant lives in xray-core).",
"example": 3427,
"type": "integer"
},
"certChainValid": { "certChainValid": {
"description": "CertChainValid ignores the name: a trusted chain presented for other names\nstill has serverNames the panel can offer instead of the failing SNI.", "description": "CertChainValid ignores the name: a trusted chain presented for other names\nstill has serverNames the panel can offer instead of the failing SNI.",
"example": true, "example": true,
@@ -3792,6 +3797,7 @@
}, },
"required": [ "required": [
"alpn", "alpn",
"certChainBytes",
"certChainValid", "certChainValid",
"certIssuer", "certIssuer",
"certSubject", "certSubject",
@@ -7240,6 +7246,7 @@
"success": true, "success": true,
"obj": { "obj": {
"alpn": "h2", "alpn": "h2",
"certChainBytes": 3427,
"certChainValid": true, "certChainValid": true,
"certIssuer": "Google Trust Services", "certIssuer": "Google Trust Services",
"certSubject": "cloudflare.com", "certSubject": "cloudflare.com",
@@ -7319,6 +7326,7 @@
"obj": [ "obj": [
{ {
"alpn": "h2", "alpn": "h2",
"certChainBytes": 3427,
"certChainValid": true, "certChainValid": true,
"certIssuer": "Google Trust Services", "certIssuer": "Google Trust Services",
"certSubject": "cloudflare.com", "certSubject": "cloudflare.com",
+1
View File
@@ -916,6 +916,7 @@ export const EXAMPLES: Record<string, unknown> = {
}, },
"RealityScanResult": { "RealityScanResult": {
"alpn": "h2", "alpn": "h2",
"certChainBytes": 3427,
"certChainValid": true, "certChainValid": true,
"certIssuer": "Google Trust Services", "certIssuer": "Google Trust Services",
"certSubject": "cloudflare.com", "certSubject": "cloudflare.com",
+6
View File
@@ -3684,6 +3684,11 @@ export const SCHEMAS: Record<string, unknown> = {
"example": "h2", "example": "h2",
"type": "string" "type": "string"
}, },
"certChainBytes": {
"description": "CertChainBytes is the sum of DER lengths of the presented peer chain.\nxray-core ML-DSA-65 REALITY needs \u003e= 3500 bytes (constant lives in xray-core).",
"example": 3427,
"type": "integer"
},
"certChainValid": { "certChainValid": {
"description": "CertChainValid ignores the name: a trusted chain presented for other names\nstill has serverNames the panel can offer instead of the failing SNI.", "description": "CertChainValid ignores the name: a trusted chain presented for other names\nstill has serverNames the panel can offer instead of the failing SNI.",
"example": true, "example": true,
@@ -3766,6 +3771,7 @@ export const SCHEMAS: Record<string, unknown> = {
}, },
"required": [ "required": [
"alpn", "alpn",
"certChainBytes",
"certChainValid", "certChainValid",
"certIssuer", "certIssuer",
"certSubject", "certSubject",
+1
View File
@@ -835,6 +835,7 @@ export interface ProbeResultUI {
export interface RealityScanResult { export interface RealityScanResult {
alpn: string; alpn: string;
certChainBytes: number;
certChainValid: boolean; certChainValid: boolean;
certIssuer: string; certIssuer: string;
certSubject: string; certSubject: string;
+1
View File
@@ -892,6 +892,7 @@ export type ProbeResultUI = z.infer<typeof ProbeResultUISchema>;
export const RealityScanResultSchema = z.object({ export const RealityScanResultSchema = z.object({
alpn: z.string(), alpn: z.string(),
certChainBytes: z.number().int(),
certChainValid: z.boolean(), certChainValid: z.boolean(),
certIssuer: z.string(), certIssuer: z.string(),
certSubject: z.string(), certSubject: z.string(),
@@ -5,11 +5,15 @@ import type { ColumnsType } from 'antd/es/table';
import type { RealityScanResult } from '@/generated/types'; import type { RealityScanResult } from '@/generated/types';
// xray-core ML-DSA-65 REALITY min peer cert-chain size (not defined in this repo).
export const MLDSA65_MIN_CERT_CHAIN_BYTES = 3500;
interface RealityTargetScannerModalProps { interface RealityTargetScannerModalProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
scanRealityCandidates: (targets?: string) => Promise<RealityScanResult[]>; scanRealityCandidates: (targets?: string) => Promise<RealityScanResult[]>;
onPick: (result: RealityScanResult) => void; onPick: (result: RealityScanResult) => void;
mldsa65Enabled?: boolean;
} }
export default function RealityTargetScannerModal({ export default function RealityTargetScannerModal({
@@ -17,6 +21,7 @@ export default function RealityTargetScannerModal({
onClose, onClose,
scanRealityCandidates, scanRealityCandidates,
onPick, onPick,
mldsa65Enabled = false,
}: RealityTargetScannerModalProps) { }: RealityTargetScannerModalProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -126,6 +131,28 @@ export default function RealityTargetScannerModal({
<Tag>{t('pages.inbounds.form.scanCertInvalid')}</Tag> <Tag>{t('pages.inbounds.form.scanCertInvalid')}</Tag>
), ),
}, },
{
title: t('pages.inbounds.form.scanCertChain'),
dataIndex: 'certChainBytes',
key: 'certChainBytes',
width: 100,
render: (bytes: number) => {
if (!bytes) return '—';
if (mldsa65Enabled && bytes < MLDSA65_MIN_CERT_CHAIN_BYTES) {
return (
<Tooltip
title={t('pages.inbounds.form.scanMldsaCertChainTooSmall', {
length: bytes,
min: MLDSA65_MIN_CERT_CHAIN_BYTES,
})}
>
<Tag color="warning">{bytes} B</Tag>
</Tooltip>
);
}
return `${bytes} B`;
},
},
{ {
title: t('pages.inbounds.form.scanLatency'), title: t('pages.inbounds.form.scanLatency'),
dataIndex: 'latencyMs', dataIndex: 'latencyMs',
@@ -165,7 +192,7 @@ export default function RealityTargetScannerModal({
</Button>, </Button>,
]} ]}
title={t('pages.inbounds.form.scanModalTitle')} title={t('pages.inbounds.form.scanModalTitle')}
width={960} width={1080}
> >
<Space orientation="vertical" size="small" style={{ width: '100%' }}> <Space orientation="vertical" size="small" style={{ width: '100%' }}>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }}> <Typography.Paragraph type="secondary" style={{ marginBottom: 0 }}>
@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Alert, Alert,
@@ -25,7 +25,9 @@ import {
validateRealityTarget, validateRealityTarget,
} from '@/lib/xray/stream-wire-normalize'; } from '@/lib/xray/stream-wire-normalize';
import type { RealityScanResult } from '@/generated/types'; import type { RealityScanResult } from '@/generated/types';
import RealityTargetScannerModal from './RealityTargetScannerModal'; import RealityTargetScannerModal, {
MLDSA65_MIN_CERT_CHAIN_BYTES,
} from './RealityTargetScannerModal';
interface RealityFormProps { interface RealityFormProps {
saving: boolean; saving: boolean;
@@ -59,6 +61,18 @@ export default function RealityForm({
const { t } = useTranslation(); const { t } = useTranslation();
const { getFieldState, trigger } = useFormContext(); const { getFieldState, trigger } = useFormContext();
const [scannerOpen, setScannerOpen] = useState(false); const [scannerOpen, setScannerOpen] = useState(false);
const mldsa65Seed = useWatch({ name: 'streamSettings.realitySettings.mldsa65Seed' });
const mldsa65Verify = useWatch({
name: 'streamSettings.realitySettings.settings.mldsa65Verify',
});
const mldsa65Enabled =
(typeof mldsa65Seed === 'string' && mldsa65Seed.trim() !== '') ||
(typeof mldsa65Verify === 'string' && mldsa65Verify.trim() !== '');
const mldsaChainTooSmall =
!!scanResult &&
mldsa65Enabled &&
scanResult.certChainBytes > 0 &&
scanResult.certChainBytes < MLDSA65_MIN_CERT_CHAIN_BYTES;
/* /*
* An untrusted certificate (self-signed fronting service on the LAN) is still * An untrusted certificate (self-signed fronting service on the LAN) is still
* worth reading, so subject/issuer stay visible and only the verdict is added. * worth reading, so subject/issuer stay visible and only the verdict is added.
@@ -130,7 +144,11 @@ export default function RealityForm({
{scanResult && ( {scanResult && (
<Form.Item label=" " colon={false}> <Form.Item label=" " colon={false}>
<Alert <Alert
type={scanResult.feasible && !scanResult.privateTarget ? 'success' : 'warning'} type={
scanResult.feasible && !scanResult.privateTarget && !mldsaChainTooSmall
? 'success'
: 'warning'
}
showIcon showIcon
title={ title={
scanResult.feasible scanResult.feasible
@@ -139,6 +157,14 @@ export default function RealityForm({
} }
description={ description={
<> <>
{mldsaChainTooSmall && (
<div style={{ marginBottom: 8 }}>
{t('pages.inbounds.form.scanMldsaCertChainTooSmall', {
length: scanResult.certChainBytes,
min: MLDSA65_MIN_CERT_CHAIN_BYTES,
})}
</div>
)}
{scanResult.privateTarget && ( {scanResult.privateTarget && (
<div style={{ marginBottom: 8 }}>{t('pages.inbounds.form.scanPrivateNote')}</div> <div style={{ marginBottom: 8 }}>{t('pages.inbounds.form.scanPrivateNote')}</div>
)} )}
@@ -159,6 +185,9 @@ export default function RealityForm({
? dayjs(scanResult.notAfter).format('YYYY-MM-DD HH:mm') ? dayjs(scanResult.notAfter).format('YYYY-MM-DD HH:mm')
: '—'} : '—'}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t('pages.inbounds.form.scanCertChain')}>
{scanResult.certChainBytes > 0 ? `${scanResult.certChainBytes} B` : '—'}
</Descriptions.Item>
<Descriptions.Item label={t('pages.inbounds.form.scanLatency')}> <Descriptions.Item label={t('pages.inbounds.form.scanLatency')}>
{scanResult.latencyMs > 0 ? `${scanResult.latencyMs} ms` : '—'} {scanResult.latencyMs > 0 ? `${scanResult.latencyMs} ms` : '—'}
</Descriptions.Item> </Descriptions.Item>
@@ -330,6 +359,7 @@ export default function RealityForm({
onClose={() => setScannerOpen(false)} onClose={() => setScannerOpen(false)}
scanRealityCandidates={scanRealityCandidates} scanRealityCandidates={scanRealityCandidates}
onPick={(r) => applyRealityScanResult(r, true)} onPick={(r) => applyRealityScanResult(r, true)}
mldsa65Enabled={mldsa65Enabled}
/> />
</> </>
); );
@@ -0,0 +1,102 @@
import { describe, it, expect } from 'vitest';
import { Form } from 'antd';
import type { ReactNode } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { RealityForm } from '@/pages/inbounds/form/security';
import RealityTargetScannerModal from '@/pages/inbounds/form/security/RealityTargetScannerModal';
import type { InboundFormValues } from '@/schemas/forms/inbound-form';
import type { RealityScanResult } from '@/generated/types';
import { renderWithProviders } from './test-utils';
const smallChain: RealityScanResult = {
alpn: 'h2',
certChainBytes: 3427,
certChainValid: true,
certIssuer: 'Google Trust Services',
certSubject: 'cloudflare.com',
certValid: true,
curveID: 'X25519',
feasible: true,
h2: true,
host: 'www.cloudflare.com',
ip: '104.16.124.96',
latencyMs: 180,
notAfter: '2026-08-01T00:00:00Z',
port: 443,
privateTarget: false,
reason: '',
serverNames: ['www.cloudflare.com'],
target: 'www.cloudflare.com:443',
tls13: true,
tlsVersion: '1.3',
x25519: true,
};
function FormHarness({
children,
defaultValues,
}: {
children: ReactNode;
defaultValues?: Record<string, unknown>;
}) {
const methods = useForm<InboundFormValues>({ defaultValues: defaultValues as never });
return (
<FormProvider {...methods}>
<Form>{children}</Form>
</FormProvider>
);
}
const noop = () => {};
function renderRealityForm(
scanResult: RealityScanResult | null,
defaultValues?: Record<string, unknown>,
) {
return renderWithProviders(
<FormHarness defaultValues={defaultValues}>
<RealityForm
saving={false}
scanning={false}
scanResult={scanResult}
scanRealityTarget={noop}
scanRealityCandidates={async () => []}
applyRealityScanResult={noop}
randomizeShortIds={noop}
randomizeSpiderX={noop}
genRealityKeypair={noop}
clearRealityKeypair={noop}
genMldsa65={noop}
clearMldsa65={noop}
/>
</FormHarness>,
);
}
describe('ML-DSA-65 cert chain warning', () => {
it('warns on the inbound form when ML-DSA-65 is on and the scanned chain is under 3500 bytes', () => {
const { getByText } = renderRealityForm(smallChain, {
streamSettings: { realitySettings: { mldsa65Seed: 'seed' } },
});
expect(getByText(/below the 3500-byte minimum required for ML-DSA-65/)).toBeTruthy();
});
it('does not warn on the inbound form when ML-DSA-65 is off', () => {
const { queryByText } = renderRealityForm(smallChain);
expect(queryByText(/below the 3500-byte minimum required for ML-DSA-65/)).toBeNull();
});
it('tags scanner rows whose cert chain is too small for ML-DSA-65', async () => {
const { findByText } = renderWithProviders(
<RealityTargetScannerModal
open
onClose={noop}
scanRealityCandidates={async () => [smallChain]}
onPick={noop}
mldsa65Enabled
/>,
);
expect(await findByText('3427 B')).toBeTruthy();
});
});
+7 -1
View File
@@ -57,7 +57,10 @@ type RealityScanResult struct {
CertValid bool `json:"certValid" example:"true"` CertValid bool `json:"certValid" example:"true"`
// CertChainValid ignores the name: a trusted chain presented for other names // CertChainValid ignores the name: a trusted chain presented for other names
// still has serverNames the panel can offer instead of the failing SNI. // still has serverNames the panel can offer instead of the failing SNI.
CertChainValid bool `json:"certChainValid" example:"true"` CertChainValid bool `json:"certChainValid" example:"true"`
// CertChainBytes is the sum of DER lengths of the presented peer chain.
// xray-core ML-DSA-65 REALITY needs >= 3500 bytes (constant lives in xray-core).
CertChainBytes int `json:"certChainBytes" example:"3427"`
CertSubject string `json:"certSubject" example:"cloudflare.com"` CertSubject string `json:"certSubject" example:"cloudflare.com"`
CertIssuer string `json:"certIssuer" example:"Google Trust Services"` CertIssuer string `json:"certIssuer" example:"Google Trust Services"`
NotAfter string `json:"notAfter" example:"2026-08-01T00:00:00Z"` NotAfter string `json:"notAfter" example:"2026-08-01T00:00:00Z"`
@@ -253,6 +256,9 @@ func (s *ServerService) probeRealityAddr(dialHost string, port int, sni string,
verifyHost := sni verifyHost := sni
if len(st.PeerCertificates) > 0 { if len(st.PeerCertificates) > 0 {
leaf := st.PeerCertificates[0] leaf := st.PeerCertificates[0]
for _, cert := range st.PeerCertificates {
res.CertChainBytes += len(cert.Raw)
}
res.CertSubject = leaf.Subject.CommonName res.CertSubject = leaf.Subject.CommonName
if res.CertSubject == "" && len(leaf.DNSNames) > 0 { if res.CertSubject == "" && len(leaf.DNSNames) > 0 {
res.CertSubject = leaf.DNSNames[0] res.CertSubject = leaf.DNSNames[0]
+2
View File
@@ -628,6 +628,8 @@
"scanCert": "الشهادة", "scanCert": "الشهادة",
"scanCertInvalid": "غير موثوق", "scanCertInvalid": "غير موثوق",
"scanCertExpiry": "انتهاء صلاحية الشهادة", "scanCertExpiry": "انتهاء صلاحية الشهادة",
"scanCertChain": "سلسلة الشهادات",
"scanMldsaCertChainTooSmall": "سلسلة شهادات الهدف {length} بايت، وهي أقل من الحد الأدنى {min} بايت المطلوب لـ ML-DSA-65. سيؤدي ذلك إلى فشل الاتصال بصمت. يُرجى اختيار هدف مختلف أو تعطيل ML-DSA-65.",
"scanSniUsed": "SNI المستخدم", "scanSniUsed": "SNI المستخدم",
"scanPrivateNote": "تم الفحص عبر شبكة خاصة/محلية — هذا العنوان غير قابل للوصول من الإنترنت.", "scanPrivateNote": "تم الفحص عبر شبكة خاصة/محلية — هذا العنوان غير قابل للوصول من الإنترنت.",
"scanPrivateConfirmTitle": "الهدف في شبكة محلية", "scanPrivateConfirmTitle": "الهدف في شبكة محلية",
+2
View File
@@ -642,6 +642,8 @@
"scanCert": "Certificate", "scanCert": "Certificate",
"scanCertInvalid": "Not trusted", "scanCertInvalid": "Not trusted",
"scanCertExpiry": "Certificate expires", "scanCertExpiry": "Certificate expires",
"scanCertChain": "Cert chain",
"scanMldsaCertChainTooSmall": "Target certificate chain is {length} bytes, which is below the {min}-byte minimum required for ML-DSA-65. This will cause silent connection failures. Please choose a different target or disable ML-DSA-65.",
"scanSniUsed": "SNI used", "scanSniUsed": "SNI used",
"scanPrivateNote": "Checked over a private/local network — this address is not reachable from the internet.", "scanPrivateNote": "Checked over a private/local network — this address is not reachable from the internet.",
"scanPrivateConfirmTitle": "Target on a local network", "scanPrivateConfirmTitle": "Target on a local network",
+2
View File
@@ -649,6 +649,8 @@
"scanCert": "Certificado", "scanCert": "Certificado",
"scanCertInvalid": "No confiable", "scanCertInvalid": "No confiable",
"scanCertExpiry": "El certificado caduca", "scanCertExpiry": "El certificado caduca",
"scanCertChain": "Cadena de certificados",
"scanMldsaCertChainTooSmall": "La cadena de certificados del destino mide {length} bytes, por debajo del mínimo de {min} bytes que requiere ML-DSA-65. Las conexiones fallarán en silencio. Elige otro destino o desactiva ML-DSA-65.",
"scanSniUsed": "SNI utilizado", "scanSniUsed": "SNI utilizado",
"scanPrivateNote": "Comprobado en una red privada/local: esta dirección no es accesible desde internet.", "scanPrivateNote": "Comprobado en una red privada/local: esta dirección no es accesible desde internet.",
"scanPrivateConfirmTitle": "Destino en una red local", "scanPrivateConfirmTitle": "Destino en una red local",
+2
View File
@@ -640,6 +640,8 @@
"scanCert": "گواهی", "scanCert": "گواهی",
"scanCertInvalid": "نامعتبر", "scanCertInvalid": "نامعتبر",
"scanCertExpiry": "انقضای گواهی", "scanCertExpiry": "انقضای گواهی",
"scanCertChain": "زنجیره گواهی",
"scanMldsaCertChainTooSmall": "زنجیره گواهی هدف {length} بایت است که کمتر از حداقل {min} بایت مورد نیاز ML-DSA-65 است. این باعث شکست بی‌صدای اتصال می‌شود. هدف دیگری انتخاب کنید یا ML-DSA-65 را غیرفعال کنید.",
"scanSniUsed": "SNI استفاده‌شده", "scanSniUsed": "SNI استفاده‌شده",
"scanPrivateNote": "بررسی از طریق شبکهٔ خصوصی/محلی انجام شد — این نشانی از اینترنت قابل دسترسی نیست.", "scanPrivateNote": "بررسی از طریق شبکهٔ خصوصی/محلی انجام شد — این نشانی از اینترنت قابل دسترسی نیست.",
"scanPrivateConfirmTitle": "هدف در شبکهٔ محلی", "scanPrivateConfirmTitle": "هدف در شبکهٔ محلی",
+2
View File
@@ -628,6 +628,8 @@
"scanCert": "Sertifikat", "scanCert": "Sertifikat",
"scanCertInvalid": "Tidak tepercaya", "scanCertInvalid": "Tidak tepercaya",
"scanCertExpiry": "Sertifikat kedaluwarsa", "scanCertExpiry": "Sertifikat kedaluwarsa",
"scanCertChain": "Rantai sertifikat",
"scanMldsaCertChainTooSmall": "Rantai sertifikat target berukuran {length} byte, di bawah minimum {min} byte yang dibutuhkan ML-DSA-65. Ini akan menyebabkan koneksi gagal tanpa pesan error. Pilih target lain atau nonaktifkan ML-DSA-65.",
"scanSniUsed": "SNI yang dipakai", "scanSniUsed": "SNI yang dipakai",
"scanPrivateNote": "Diperiksa melalui jaringan privat/lokal — alamat ini tidak dapat dijangkau dari internet.", "scanPrivateNote": "Diperiksa melalui jaringan privat/lokal — alamat ini tidak dapat dijangkau dari internet.",
"scanPrivateConfirmTitle": "Target di jaringan lokal", "scanPrivateConfirmTitle": "Target di jaringan lokal",
+2
View File
@@ -649,6 +649,8 @@
"scanCert": "証明書", "scanCert": "証明書",
"scanCertInvalid": "信頼できません", "scanCertInvalid": "信頼できません",
"scanCertExpiry": "証明書の有効期限", "scanCertExpiry": "証明書の有効期限",
"scanCertChain": "証明書チェーン",
"scanMldsaCertChainTooSmall": "ターゲットの証明書チェーンは {length} バイトで、ML-DSA-65 に必要な下限 {min} バイトを下回っています。接続はエラーなく失敗します。別のターゲットを選ぶか ML-DSA-65 を無効にしてください。",
"scanSniUsed": "使用した SNI", "scanSniUsed": "使用した SNI",
"scanPrivateNote": "プライベート/ローカルネットワーク経由で確認しました。このアドレスはインターネットからは到達できません。", "scanPrivateNote": "プライベート/ローカルネットワーク経由で確認しました。このアドレスはインターネットからは到達できません。",
"scanPrivateConfirmTitle": "ローカルネットワーク上のターゲット", "scanPrivateConfirmTitle": "ローカルネットワーク上のターゲット",
+2
View File
@@ -649,6 +649,8 @@
"scanCert": "Certificado", "scanCert": "Certificado",
"scanCertInvalid": "Não confiável", "scanCertInvalid": "Não confiável",
"scanCertExpiry": "Certificado expira", "scanCertExpiry": "Certificado expira",
"scanCertChain": "Cadeia de certificados",
"scanMldsaCertChainTooSmall": "A cadeia de certificados do destino tem {length} bytes, abaixo do mínimo de {min} bytes exigido pelo ML-DSA-65. Isso causará falhas silenciosas de conexão. Escolha outro destino ou desative o ML-DSA-65.",
"scanSniUsed": "SNI utilizado", "scanSniUsed": "SNI utilizado",
"scanPrivateNote": "Verificado em uma rede privada/local — este endereço não é acessível pela internet.", "scanPrivateNote": "Verificado em uma rede privada/local — este endereço não é acessível pela internet.",
"scanPrivateConfirmTitle": "Destino em uma rede local", "scanPrivateConfirmTitle": "Destino em uma rede local",
+2
View File
@@ -651,6 +651,8 @@
"scanCert": "Сертификат", "scanCert": "Сертификат",
"scanCertInvalid": "Не доверенный", "scanCertInvalid": "Не доверенный",
"scanCertExpiry": "Сертификат истекает", "scanCertExpiry": "Сертификат истекает",
"scanCertChain": "Цепочка сертификатов",
"scanMldsaCertChainTooSmall": "Цепочка сертификатов цели занимает {length} байт — меньше минимума {min} байт, требуемого для ML-DSA-65. Соединения будут молча уходить в fallback. Выберите другую цель или отключите ML-DSA-65.",
"scanSniUsed": "Использованный SNI", "scanSniUsed": "Использованный SNI",
"scanPrivateNote": "Проверено во внутренней (локальной) сети — этот адрес недоступен из интернета.", "scanPrivateNote": "Проверено во внутренней (локальной) сети — этот адрес недоступен из интернета.",
"scanPrivateConfirmTitle": "Цель в локальной сети", "scanPrivateConfirmTitle": "Цель в локальной сети",
+2
View File
@@ -628,6 +628,8 @@
"scanCert": "Sertifika", "scanCert": "Sertifika",
"scanCertInvalid": "Güvenilmez", "scanCertInvalid": "Güvenilmez",
"scanCertExpiry": "Sertifika bitiş tarihi", "scanCertExpiry": "Sertifika bitiş tarihi",
"scanCertChain": "Sertifika zinciri",
"scanMldsaCertChainTooSmall": "Hedef sertifika zinciri {length} bayt; ML-DSA-65 için gereken {min} bayt minimumunun altında. Bu, bağlantıların sessizce başarısız olmasına yol açar. Farklı bir hedef seçin veya ML-DSA-65'i kapatın.",
"scanSniUsed": "Kullanılan SNI", "scanSniUsed": "Kullanılan SNI",
"scanPrivateNote": "Özel/yerel ağ üzerinden kontrol edildi — bu adrese internetten erişilemez.", "scanPrivateNote": "Özel/yerel ağ üzerinden kontrol edildi — bu adrese internetten erişilemez.",
"scanPrivateConfirmTitle": "Hedef yerel ağda", "scanPrivateConfirmTitle": "Hedef yerel ağda",
+2
View File
@@ -628,6 +628,8 @@
"scanCert": "Сертифікат", "scanCert": "Сертифікат",
"scanCertInvalid": "Ненадійний", "scanCertInvalid": "Ненадійний",
"scanCertExpiry": "Сертифікат діє до", "scanCertExpiry": "Сертифікат діє до",
"scanCertChain": "Ланцюжок сертифікатів",
"scanMldsaCertChainTooSmall": "Ланцюжок сертифікатів цілі має {length} байт, що менше мінімуму {min} байт для ML-DSA-65. З’єднання мовчки потраплятимуть у fallback. Оберіть іншу ціль або вимкніть ML-DSA-65.",
"scanSniUsed": "Використаний SNI", "scanSniUsed": "Використаний SNI",
"scanPrivateNote": "Перевірено у внутрішній (локальній) мережі — ця адреса недоступна з інтернету.", "scanPrivateNote": "Перевірено у внутрішній (локальній) мережі — ця адреса недоступна з інтернету.",
"scanPrivateConfirmTitle": "Ціль у локальній мережі", "scanPrivateConfirmTitle": "Ціль у локальній мережі",
+2
View File
@@ -649,6 +649,8 @@
"scanCert": "Chứng chỉ", "scanCert": "Chứng chỉ",
"scanCertInvalid": "Không tin cậy", "scanCertInvalid": "Không tin cậy",
"scanCertExpiry": "Chứng chỉ hết hạn", "scanCertExpiry": "Chứng chỉ hết hạn",
"scanCertChain": "Chuỗi chứng chỉ",
"scanMldsaCertChainTooSmall": "Chuỗi chứng chỉ của đích dài {length} byte, thấp hơn mức tối thiểu {min} byte mà ML-DSA-65 yêu cầu. Kết nối sẽ thất bại im lặng. Hãy chọn đích khác hoặc tắt ML-DSA-65.",
"scanSniUsed": "SNI đã dùng", "scanSniUsed": "SNI đã dùng",
"scanPrivateNote": "Đã kiểm tra qua mạng riêng/nội bộ — địa chỉ này không truy cập được từ internet.", "scanPrivateNote": "Đã kiểm tra qua mạng riêng/nội bộ — địa chỉ này không truy cập được từ internet.",
"scanPrivateConfirmTitle": "Đích trong mạng nội bộ", "scanPrivateConfirmTitle": "Đích trong mạng nội bộ",
+2
View File
@@ -648,6 +648,8 @@
"scanCert": "证书", "scanCert": "证书",
"scanCertInvalid": "不受信任", "scanCertInvalid": "不受信任",
"scanCertExpiry": "证书有效期至", "scanCertExpiry": "证书有效期至",
"scanCertChain": "证书链",
"scanMldsaCertChainTooSmall": "目标证书链为 {length} 字节,低于 ML-DSA-65 所需的 {min} 字节下限。这会导致连接静默失败。请更换目标或关闭 ML-DSA-65。",
"scanSniUsed": "使用的 SNI", "scanSniUsed": "使用的 SNI",
"scanPrivateNote": "已通过内网/本地网络检测 — 该地址无法从互联网访问。", "scanPrivateNote": "已通过内网/本地网络检测 — 该地址无法从互联网访问。",
"scanPrivateConfirmTitle": "目标位于本地网络", "scanPrivateConfirmTitle": "目标位于本地网络",
+2
View File
@@ -628,6 +628,8 @@
"scanCert": "憑證", "scanCert": "憑證",
"scanCertInvalid": "不受信任", "scanCertInvalid": "不受信任",
"scanCertExpiry": "憑證有效期限", "scanCertExpiry": "憑證有效期限",
"scanCertChain": "憑證鏈",
"scanMldsaCertChainTooSmall": "目標憑證鏈為 {length} 位元組,低於 ML-DSA-65 所需的 {min} 位元組下限。這會導致連線靜默失敗。請更換目標或關閉 ML-DSA-65。",
"scanSniUsed": "使用的 SNI", "scanSniUsed": "使用的 SNI",
"scanPrivateNote": "已透過內網/本機網路檢測 — 此位址無法從網際網路存取。", "scanPrivateNote": "已透過內網/本機網路檢測 — 此位址無法從網際網路存取。",
"scanPrivateConfirmTitle": "目標位於本機網路", "scanPrivateConfirmTitle": "目標位於本機網路",