Files
teleport/mpc/__tests__/profile_negative.test.ts
zeekay 3dcc1cf6a4 tests: Tier A e2e + fuzz across bridge / custody-MPC / oracle / profile-gate
Adds the test suite that backs the SUBMISSION / PROOF-CLAIMS package.

contracts/test/Bridge.e2e.test.ts (14 cases):
  - Full burn -> envelope -> claim round-trip under classical-compat.
  - Off-chain reconstruction of messageHash from BridgeBurnedV2 logs.
  - P3 envelope binding: tampering amount / recipient / nonce / token /
    sourceChainId / destChainId all refused with EnvelopeBindingMismatch
    or InvalidChainId.
  - P1 strict-PQ profile flip: classical -> strict-PQ -> rollback.
  - P5 EXTCODEHASH allow-list defence: ADMIN_ROLE compromise cannot
    install a rogue verifier even with an allow-listed policyId.
  - Pausable round-trip.

mpc/__tests__/custody_mpc_e2e.test.ts (12 cases):
  - policyId of every scheme matches @luxfi/pq-profile-ids-ts (single
    source of truth shared with on-chain StrictPQProfileGate).
  - LUX_PROFILE=quasar-strict-pq default: double-lattice round-trip
    via inproc mocks (both pulsar + corona daemons called).
  - LUX_PROFILE=classical-compat default: ecdsa-GG18 wire byte-identical.
  - Per-scheme sign/verify round-trip: frost (real noble-curves),
    cggmp21 / pulsar / corona / double-lattice (inproc mocks).
  - double-lattice composition contract: inner Pulsar + Corona exposed
    via readonly fields, sub-sig split unambiguous.

mpc/__tests__/profile_negative.test.ts (28 cases):
  - Full 4 x (6+1) profile x scheme matrix exhaustively exercised.
  - Fail-closed on unset LUX_PROFILE regardless of MPC_SCHEME value
    (closes Red HIGH #4).
  - Unknown profile / unknown scheme rejected with clear errors.

mpc/__tests__/signature_fuzz.test.ts (8 fuzz harnesses):
  - hex / unhex round-trip across 1024 random byte arrays.
  - double-lattice split unambiguous across 256 random sub-sig
    lengths in [16, 256] bytes.
  - Truncation / bit-flip / lane-swap rejection.
  - JSON-RPC hex validation across 256 random message sizes.
  - Wrong-key rejection on Corona with 32 fuzz cases.
  - Empty sub-signature refused (defensive).

api/src/pq/oracle_attestation.test.ts (24 cases):
  - WALLET_SCHEME_MLDSA65 == 0x42 byte-pin (Go-side wire-compat).
  - CSHAKE_CUSTOMIZATION_TX byte-pin (16-byte ASCII).
  - serializeEnvelope round-trip + edge values + out-of-range refusal.
  - signEnvelope dispatch refuses non-MLDSA65 account / provider.
  - envelopeDigest is byte-identical to cshake256(envelope, N, S, 48).
  - LUX_PROFILE / STRICT_PQ resolution under all four cases.

Totals:
  - contracts:  60 existing + 14 new = 74 passing
  - mpc:        31 existing + 58 new = 89 passing
  - api:        19 existing + 24 new = 43 passing

  Repo-wide: 206 tests passing.
2026-05-19 08:41:24 -07:00

139 lines
4.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Profile-gate negative tests.
*
* Exhaustively exercises the 4 × 6 (profile × scheme) matrix from
* SPEC §4.2 to assert that:
*
* - Every (profile, scheme) cell in the admissible set returns the
* expected scheme.
* - Every (profile, scheme) cell outside the admissible set throws
* with a clear error message.
*
* This is the latch for Red HIGH #4 closure plus its symmetric leg
* (PQ schemes under classical-compat throw). The 24-cell matrix
* makes drift impossible to slip through.
*/
import { resolveSchemeFromEnv } from '../src/signers';
type Profile = 'quasar-strict-pq' | 'classical-compat' | 'hybrid';
type Scheme =
| 'ecdsa'
| 'cggmp21'
| 'frost'
| 'pulsar'
| 'corona'
| 'double-lattice';
const PROFILES: Profile[] = ['quasar-strict-pq', 'classical-compat', 'hybrid'];
const SCHEMES: Scheme[] = ['ecdsa', 'cggmp21', 'frost', 'pulsar', 'corona', 'double-lattice'];
const CLASSICAL: Set<Scheme> = new Set(['ecdsa', 'cggmp21', 'frost']);
const PQ: Set<Scheme> = new Set(['pulsar', 'corona', 'double-lattice']);
/**
* Authoritative admissibility table (matches SPEC §4.2).
*
* `null` means the resolver throws; otherwise it returns the cell value.
*
* Note: under `hybrid` the resolver emits a WARN (tested elsewhere)
* but still resolves to the requested scheme — hybrid is permissive
* by design.
*/
function admissible(profile: Profile, scheme: Scheme | undefined): Scheme | null {
if (profile === 'quasar-strict-pq') {
if (scheme === undefined) return 'double-lattice';
return PQ.has(scheme) ? scheme : null;
}
if (profile === 'classical-compat') {
if (scheme === undefined) return 'ecdsa';
return CLASSICAL.has(scheme) ? scheme : null;
}
// hybrid
if (scheme === undefined) return 'ecdsa';
return scheme;
}
describe('profile-gate negative matrix — full 4 × (6+1)', () => {
// 4 profiles × (6 schemes + 1 "scheme unset") = 28 cells. We assert
// the full matrix against the SPEC table.
for (const profile of PROFILES) {
for (const scheme of [undefined, ...SCHEMES] as const) {
const expected = admissible(profile, scheme);
const cell = scheme === undefined ? '<unset>' : scheme;
if (expected === null) {
it(`(profile=${profile}, scheme=${cell}) MUST throw "incompatible"`, () => {
expect(() =>
resolveSchemeFromEnv({
LUX_PROFILE: profile,
...(scheme ? { MPC_SCHEME: scheme } : {}),
} as never),
).toThrow(/incompatible/);
});
} else {
it(`(profile=${profile}, scheme=${cell}) MUST resolve to ${expected}`, () => {
// Suppress hybrid WARN noise.
const spy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
try {
const got = resolveSchemeFromEnv({
LUX_PROFILE: profile,
...(scheme ? { MPC_SCHEME: scheme } : {}),
} as never);
expect(got).toBe(expected);
} finally {
spy.mockRestore();
}
});
}
}
}
});
describe('profile-gate: fail-closed on unset LUX_PROFILE', () => {
// No matter what MPC_SCHEME is set to, an unset LUX_PROFILE throws.
// This is the Red HIGH #4 closure: the previous behaviour of
// "silent ECDSA when env is unset" cannot recur.
it.each([
{},
{ MPC_SCHEME: 'ecdsa' },
{ MPC_SCHEME: 'pulsar' },
{ MPC_SCHEME: 'double-lattice' },
{ MPC_SCHEME: 'frost' },
{ MPC_SCHEME: 'cggmp21' },
{ MPC_SCHEME: 'corona' },
])('env=%j throws with explicit LUX_PROFILE remediation', (env) => {
expect(() => resolveSchemeFromEnv(env as never)).toThrow(/LUX_PROFILE must be set/);
});
});
describe('profile-gate: unknown values rejected explicitly', () => {
it('unknown profile throws with allow-list', () => {
expect(() => resolveSchemeFromEnv({ LUX_PROFILE: 'rubbish' } as never)).toThrow(
/is not one of/,
);
});
it('unknown scheme throws BEFORE profile coherence check', () => {
// LUX_PROFILE is valid but MPC_SCHEME is bogus — must throw the
// scheme-validation error first, not the coherence error.
expect(() =>
resolveSchemeFromEnv({
LUX_PROFILE: 'classical-compat',
MPC_SCHEME: 'bogus',
} as never),
).toThrow(/MPC_SCHEME='bogus' is not one of/);
});
it('case-insensitive after trim', () => {
// Demonstrate that 'FROST' / ' FROST ' resolve identically to 'frost'
// — defence against accidental capitalisation in env vars.
expect(
resolveSchemeFromEnv({
LUX_PROFILE: 'classical-compat',
MPC_SCHEME: ' FROST ',
} as never),
).toBe('frost');
});
});