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