Complete native QUIC certificate validation

This commit is contained in:
Meta Vens 2026-07-20 03:43:05 +01:00
commit 5e742c26ae
3 changed files with 97 additions and 5 deletions

View file

@ -11,6 +11,7 @@
#include <deque>
#include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <vector>
@ -58,8 +59,22 @@ struct Client {
std::mutex gate;
std::deque<std::vector<uint8_t>> received;
std::string error;
std::string trace;
};
void AppendTrace(Client* client, const std::string& event) {
if (client == nullptr || client->state.load() == ClientState::Connected) return;
std::lock_guard<std::mutex> lock(client->gate);
if (!client->trace.empty()) client->trace += ",";
client->trace += event;
}
std::string TraceSnapshot(Client* client) {
if (client == nullptr) return "none";
std::lock_guard<std::mutex> lock(client->gate);
return client->trace.empty() ? "none" : client->trace;
}
void SetFailure(Client* client, const std::string& message) {
if (client == nullptr || client->closing.load()) return;
{
@ -184,8 +199,30 @@ bool CertificateMatchesPin(Client* client, QUIC_CERTIFICATE* certificate) {
return std::memcmp(digest, client->certificate_pin.data(), sizeof(digest)) == 0;
}
std::string Hex(uint64_t value) {
std::ostringstream text;
text << "0x" << std::hex << std::uppercase << value;
return text.str();
}
std::string LoadedMsQuicPath() {
const auto module = GetModuleHandleW(L"msquic.dll");
if (module == nullptr) return "unresolved";
std::array<wchar_t, 32768> path{};
const auto length = GetModuleFileNameW(module, path.data(), static_cast<DWORD>(path.size()));
if (length == 0 || length >= path.size()) return "unresolved";
const auto required = WideCharToMultiByte(
CP_UTF8, 0, path.data(), static_cast<int>(length), nullptr, 0, nullptr, nullptr);
if (required <= 0) return "unresolved";
std::string utf8(static_cast<size_t>(required), '\0');
WideCharToMultiByte(
CP_UTF8, 0, path.data(), static_cast<int>(length), utf8.data(), required, nullptr, nullptr);
return utf8;
}
QUIC_STATUS QUIC_API ConnectionCallback(HQUIC connection, void* context, QUIC_CONNECTION_EVENT* event) {
auto* client = static_cast<Client*>(context);
AppendTrace(client, "event=" + std::to_string(static_cast<int>(event->Type)));
switch (event->Type) {
case QUIC_CONNECTION_EVENT_CONNECTED:
client->state.store(ClientState::Connected);
@ -200,14 +237,40 @@ QUIC_STATUS QUIC_API ConnectionCallback(HQUIC connection, void* context, QUIC_CO
break;
}
case QUIC_CONNECTION_EVENT_PEER_CERTIFICATE_RECEIVED:
if (!CertificateMatchesPin(client, event->PEER_CERTIFICATE_RECEIVED.Certificate)) {
{
AppendTrace(
client,
"certificate-status=" +
Hex(static_cast<uint64_t>(event->PEER_CERTIFICATE_RECEIVED.DeferredStatus)));
const auto matches = CertificateMatchesPin(
client,
event->PEER_CERTIFICATE_RECEIVED.Certificate);
if (!matches) {
SetFailure(client, "CultMesh QUIC provider certificate does not match the advertised SHA-256 pin.");
return QUIC_STATUS_BAD_CERTIFICATE;
}
return QUIC_STATUS_SUCCESS;
const auto completion = client->api->ConnectionCertificateValidationComplete(
connection,
matches ? TRUE : FALSE,
matches ? QUIC_TLS_ALERT_CODE_SUCCESS : QUIC_TLS_ALERT_CODE_BAD_CERTIFICATE);
if (QUIC_FAILED(completion)) {
SetFailure(
client,
"CultMesh QUIC certificate validation completion failed (status=" +
Hex(static_cast<uint64_t>(completion)) + ").");
return completion;
}
return QUIC_STATUS_PENDING;
}
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
if (!client->closing.load())
SetFailure(client, "CultMesh QUIC connection was shut down by the transport.");
if (!client->closing.load()) {
SetFailure(
client,
"CultMesh QUIC connection was shut down by the transport "
"(status=" + Hex(static_cast<uint64_t>(event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status)) +
", error=" + Hex(event->SHUTDOWN_INITIATED_BY_TRANSPORT.ErrorCode) +
", msquic=" + LoadedMsQuicPath() +
", events=" + TraceSnapshot(client) + ").");
}
break;
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER:
if (!client->closing.load())

View file

@ -94,9 +94,18 @@ authentication to provider authorization. Test-only trust-all callbacks are not
a release configuration. A trusted discovery record may carry
`cert-sha256=<fingerprint>` on its QUIC route; absent a caller validator, the
connector accepts a self-signed provider certificate only when that pin matches.
The native client uses MsQuic's deferred-certificate contract: it completes the
pin decision through `ConnectionCertificateValidationComplete` and returns
`QUIC_STATUS_PENDING` from the certificate event. Transport failures report the
MsQuic status, wire error, loaded runtime path, and pre-connect event sequence.
## Local parity evidence
The native suite can also connect to an independently running managed provider
by setting `CULTMESH_NATIVE_EXTERNAL_ENDPOINT` to its advertised pinned QUIC
route. This keeps the managed server and native client in separate processes,
so one process-local MsQuic DLL cannot counterfeit cross-runtime parity.
On 2026-07-19, the parity harness moved a 13 MiB payload on loopback using
1 MiB content chunks. Raw TCP reached 127.4 MiB/s, verified CultMesh TCP content
reached 59.0 MiB/s, raw MsQuic reached 51.5 MiB/s, and legacy CultMesh RUDP

View file

@ -12,6 +12,26 @@ namespace GameCult.Mesh.Quic.Native.Tests;
[TestFixture]
public sealed class CultMeshNativeQuicRealtimeTransportTests
{
[Test]
public async Task NativeConnectorReceivesFromExternalManagedProvider()
{
var endpoint = Environment.GetEnvironmentVariable("CULTMESH_NATIVE_EXTERNAL_ENDPOINT");
if (string.IsNullOrWhiteSpace(endpoint))
Assert.Ignore("Set CULTMESH_NATIVE_EXTERNAL_ENDPOINT to exercise cross-process native/managed parity.");
var connector = new CultMeshNativeQuicRealtimeTransportConnector();
using var client = await connector.ConnectAsync(
new CultMeshTransportCandidate(endpoint!),
CultMeshEndpointId.Parse("aetheria.local"));
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var frame = await client.ReceiveAsync(timeout.Token);
client.TransportId.Should().Be("msquic-native-realtime");
frame.SchemaId.Should().NotBeNullOrWhiteSpace();
frame.BodyId.Should().NotBeNullOrWhiteSpace();
frame.Payload.Should().NotBeEmpty();
}
[Test]
public async Task NativeConnectorAuthenticatesAndReceivesLatestOnlyFrame()
{