diff --git a/native/GameCult.Mesh.Quic.Native/CMakeLists.txt b/native/GameCult.Mesh.Quic.Native/CMakeLists.txt index f08b2f4a..936002cc 100644 --- a/native/GameCult.Mesh.Quic.Native/CMakeLists.txt +++ b/native/GameCult.Mesh.Quic.Native/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.24) project(gamecult_mesh_quic_native LANGUAGES CXX) -# Two platforms, one source. Windows takes MsQuic from the pinned Schannel NuGet +# Two platforms, one source. Windows takes MsQuic from the pinned OpenSSL NuGet # layout (MSQUIC_ROOT); Linux takes it from the extracted libmsquic deb, whose # headers and library land in unrelated places, so they are named separately. if(NOT WIN32 AND NOT (CMAKE_SYSTEM_NAME STREQUAL "Linux")) @@ -16,8 +16,13 @@ target_include_directories(gamecult_mesh_quic_native PUBLIC include) if(WIN32) if(NOT DEFINED MSQUIC_ROOT) - message(FATAL_ERROR "MSQUIC_ROOT must point at the extracted Microsoft.Native.Quic.MsQuic.Schannel package build/native directory") + message(FATAL_ERROR "MSQUIC_ROOT must point at the extracted Microsoft.Native.Quic.MsQuic.OpenSSL package build/native directory") endif() + # Static CRT. A Node or Unity host without the Visual C++ redistributable + # installed cannot load a DLL that imports vcruntime140.dll, and the failure + # arrives as "the specified module could not be found" with nothing naming the + # missing runtime. The bridge carries its own instead. + set_property(TARGET gamecult_mesh_quic_native PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") target_compile_definitions(gamecult_mesh_quic_native PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX) target_include_directories(gamecult_mesh_quic_native PRIVATE "${MSQUIC_ROOT}/include") target_link_directories(gamecult_mesh_quic_native PRIVATE "${MSQUIC_ROOT}/lib/x64") diff --git a/native/GameCult.Mesh.Quic.Native/README.md b/native/GameCult.Mesh.Quic.Native/README.md index 4f6fa498..adc256a0 100644 --- a/native/GameCult.Mesh.Quic.Native/README.md +++ b/native/GameCult.Mesh.Quic.Native/README.md @@ -69,8 +69,17 @@ Windows x64, from the repo root: powershell -File scripts/build-quic-native.ps1 -MsQuic comes from a pinned NuGet package verified by SHA-256, and the build uses -MSVC with `/Brepro` so a rebuild reproduces the committed Unity plugin DLL. +MsQuic comes from `Microsoft.Native.Quic.MsQuic.OpenSSL` 2.5.9, pinned twice: the +NuGet zip by digest and size, and the `msquic.dll` inside it by digest and size, +because the archive is what the download is checked against and the DLL is what +actually ships beside the bridge. The OpenSSL flavour rather than Schannel +because Schannel's MsQuic refuses `QUIC_CREDENTIAL_TYPE_CERTIFICATE_PKCS12`, +which is the only credential type a provider has on both platforms. + +The build uses MSVC with `/Brepro` so a rebuild reproduces the committed Unity +plugin DLL, and links the CRT statically (`/MT`). A Node or Unity host without +the Visual C++ redistributable cannot load a DLL that imports `vcruntime140.dll`, +and the failure it reports names no missing runtime; the bridge carries its own. Linux x64, on a Debian 13 host: diff --git a/scripts/build-quic-native.ps1 b/scripts/build-quic-native.ps1 index f1b35787..553152d0 100644 --- a/scripts/build-quic-native.ps1 +++ b/scripts/build-quic-native.ps1 @@ -14,9 +14,21 @@ if ($Architecture -ne "x64") { $repoRoot = Split-Path -Parent $PSScriptRoot $version = "2.5.9" -$expectedPackageSha256 = "9877A919E1AA73AA4800F1E8A06B6539021F376228910138EB04540CD956617B" -$cacheRoot = Join-Path $repoRoot "artifacts\dependencies\msquic-schannel-$version" -$packagePath = Join-Path $cacheRoot "Microsoft.Native.Quic.MsQuic.Schannel.$version.zip" +# OpenSSL, not Schannel (Q9 ruled A). The Schannel build of MsQuic refuses +# QUIC_CREDENTIAL_TYPE_CERTIFICATE_PKCS12 with QUIC_STATUS_NOT_SUPPORTED, which +# is the only credential type a provider has on both platforms, so a Windows +# provider could not open a listener at all. The identical bridge binary passes +# the whole v2 smoke and the v1 gate against this runtime. +# +# Two digests, because they answer different questions. The zip is what the +# download is pinned to; the DLL is what actually ships beside the bridge, and +# pinning only the archive leaves the file nobody checked. +$expectedPackageSha256 = "FEE9A664C0052DEEE66D40CED2E19BFE20BC423DE5AE5234CFC0CEBC22CA3F5E" +$expectedPackageBytes = 19179817 +$expectedRuntimeSha256 = "C17C65813FE007EB486A5270D244F3659261355795B27ABB99BD19FF77539263" +$expectedRuntimeBytes = 4181856 +$cacheRoot = Join-Path $repoRoot "artifacts\dependencies\msquic-openssl-$version" +$packagePath = Join-Path $cacheRoot "Microsoft.Native.Quic.MsQuic.OpenSSL.$version.zip" $extractRoot = Join-Path $cacheRoot "package" $nativeRoot = Join-Path $extractRoot "build\native" $buildRoot = Join-Path $repoRoot "artifacts\quic-native-build\$Architecture\$Configuration" @@ -26,27 +38,36 @@ $outputRoot = if ([IO.Path]::IsPathRooted($OutputDirectory)) { Join-Path $repoRoot $OutputDirectory } +function Assert-Pinned { + param([string] $Path, [string] $ExpectedSha256, [long] $ExpectedBytes, [string] $What) + $actualBytes = (Get-Item -LiteralPath $Path).Length + $sha256 = [Security.Cryptography.SHA256]::Create() + try { + $stream = [IO.File]::OpenRead($Path) + try { $actualSha256 = [BitConverter]::ToString($sha256.ComputeHash($stream)).Replace("-", "") } + finally { $stream.Dispose() } + } finally { $sha256.Dispose() } + if ($actualSha256 -ne $ExpectedSha256 -or $actualBytes -ne $ExpectedBytes) { + throw ("$What does not match the pinned release: " + + "expected $ExpectedSha256 ($ExpectedBytes bytes), got $actualSha256 ($actualBytes bytes).") + } +} + New-Item -ItemType Directory -Force -Path $cacheRoot | Out-Null if (-not (Test-Path -LiteralPath $packagePath)) { & curl.exe -L --fail --silent --show-error ` -o $packagePath ` - "https://www.nuget.org/api/v2/package/Microsoft.Native.Quic.MsQuic.Schannel/$version" + "https://www.nuget.org/api/v2/package/Microsoft.Native.Quic.MsQuic.OpenSSL/$version" if ($LASTEXITCODE -ne 0) { throw "MsQuic package download failed with exit code $LASTEXITCODE" } } -$sha256 = [Security.Cryptography.SHA256]::Create() -try { - $packageBytes = [IO.File]::ReadAllBytes($packagePath) - $packageSha256 = [BitConverter]::ToString($sha256.ComputeHash($packageBytes)).Replace("-", "") -} finally { - $sha256.Dispose() -} -if ($packageSha256 -ne $expectedPackageSha256) { - throw "MsQuic package digest does not match the pinned release." -} +Assert-Pinned -Path $packagePath -ExpectedSha256 $expectedPackageSha256 ` + -ExpectedBytes $expectedPackageBytes -What "The MsQuic NuGet package" if (-not (Test-Path -LiteralPath (Join-Path $nativeRoot "include\msquic.h"))) { if (Test-Path -LiteralPath $extractRoot) { Remove-Item -LiteralPath $extractRoot -Recurse -Force } Expand-Archive -LiteralPath $packagePath -DestinationPath $extractRoot } +Assert-Pinned -Path (Join-Path $nativeRoot "bin\x64\msquic.dll") ` + -ExpectedSha256 $expectedRuntimeSha256 -ExpectedBytes $expectedRuntimeBytes -What "The MsQuic runtime DLL" cmake -S (Join-Path $repoRoot "native\GameCult.Mesh.Quic.Native") -B $buildRoot -A x64 ` "-DMSQUIC_ROOT=$($nativeRoot.Replace('\', '/'))" diff --git a/scripts/build-unity-package.ps1 b/scripts/build-unity-package.ps1 index 8e425066..377a1586 100644 --- a/scripts/build-unity-package.ps1 +++ b/scripts/build-unity-package.ps1 @@ -193,4 +193,4 @@ $manifest = Get-Content -LiteralPath (Join-Path $outputRoot "package.json") -Raw Write-Host "CultLib Unity package: $outputRoot" Write-Host "Package: $($manifest.name)@$($manifest.version)" Write-Host "Managed assemblies: $($expectedAssemblies.Count)" -Write-Host "Native realtime: MsQuic Schannel 2.5.9 (Windows x64)" +Write-Host "Native realtime: MsQuic OpenSSL 2.5.9 (Windows x64)"