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 <deque>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <sstream>
#include <string> #include <string>
#include <vector> #include <vector>
@ -58,8 +59,22 @@ struct Client {
std::mutex gate; std::mutex gate;
std::deque<std::vector<uint8_t>> received; std::deque<std::vector<uint8_t>> received;
std::string error; 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) { void SetFailure(Client* client, const std::string& message) {
if (client == nullptr || client->closing.load()) return; 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; 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) { QUIC_STATUS QUIC_API ConnectionCallback(HQUIC connection, void* context, QUIC_CONNECTION_EVENT* event) {
auto* client = static_cast<Client*>(context); auto* client = static_cast<Client*>(context);
AppendTrace(client, "event=" + std::to_string(static_cast<int>(event->Type)));
switch (event->Type) { switch (event->Type) {
case QUIC_CONNECTION_EVENT_CONNECTED: case QUIC_CONNECTION_EVENT_CONNECTED:
client->state.store(ClientState::Connected); client->state.store(ClientState::Connected);
@ -200,14 +237,40 @@ QUIC_STATUS QUIC_API ConnectionCallback(HQUIC connection, void* context, QUIC_CO
break; break;
} }
case QUIC_CONNECTION_EVENT_PEER_CERTIFICATE_RECEIVED: 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."); 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: case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
if (!client->closing.load()) if (!client->closing.load()) {
SetFailure(client, "CultMesh QUIC connection was shut down by the transport."); 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; break;
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER: case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER:
if (!client->closing.load()) 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 a release configuration. A trusted discovery record may carry
`cert-sha256=<fingerprint>` on its QUIC route; absent a caller validator, the `cert-sha256=<fingerprint>` on its QUIC route; absent a caller validator, the
connector accepts a self-signed provider certificate only when that pin matches. 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 ## 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 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 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 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] [TestFixture]
public sealed class CultMeshNativeQuicRealtimeTransportTests 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] [Test]
public async Task NativeConnectorAuthenticatesAndReceivesLatestOnlyFrame() public async Task NativeConnectorAuthenticatesAndReceivesLatestOnlyFrame()
{ {