From 9a767e1b4ddff5080fdff44e3a825fe7c13f8564 Mon Sep 17 00:00:00 2001 From: Kris Pennington Date: Wed, 2 Sep 2026 10:26:03 -0500 Subject: [PATCH 1/2] Add ARM64 attestation packaging and validation for HVCI install Introduce CI/Release driver package validators, Partner Center CAB creation, ARM64 acceptance harness, and CI artifact upload so maintainers can submit a Microsoft-signed ARM64 preview for issue #465. Co-authored-by: Cursor --- .github/workflows/ci-validation.yml | 28 ++ docs/windows-arm64-signing.md | 121 ++++++++ scripts/Invoke-Arm64AcceptanceTest.ps1 | 202 +++++++++++++ scripts/New-AttestationCab.ps1 | 164 +++++++++++ scripts/Test-Arm64DriverPackage.ps1 | 383 +++++++++++++++++++++++++ 5 files changed, 898 insertions(+) create mode 100644 docs/windows-arm64-signing.md create mode 100644 scripts/Invoke-Arm64AcceptanceTest.ps1 create mode 100644 scripts/New-AttestationCab.ps1 create mode 100644 scripts/Test-Arm64DriverPackage.ps1 diff --git a/.github/workflows/ci-validation.yml b/.github/workflows/ci-validation.yml index b82179c..3f69a1a 100644 --- a/.github/workflows/ci-validation.yml +++ b/.github/workflows/ci-validation.yml @@ -142,9 +142,37 @@ jobs: New-Item -ItemType Directory -Path $dest -Force | Out-Null Copy-Item "$outDir\*" -Destination $dest -Recurse -Force + - name: Validate ARM64 driver package (CI policy) + if: matrix.platform == 'ARM64' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + ./scripts/Test-Arm64DriverPackage.ps1 ` + -PackagePath "artifacts/VDD/ARM64" ` + -Policy CI + + - name: Create ARM64 attestation CAB + if: matrix.platform == 'ARM64' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $result = ./scripts/New-AttestationCab.ps1 ` + -InputDirectory "artifacts/VDD/ARM64" ` + -OutputDirectory "artifacts/VDD/ARM64" ` + -CabFileName "MttVDD-ARM64-Attestation.cab" + Write-Output "Created attestation CAB at $($result.CabPath)" + - name: Upload artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: VDD-${{ matrix.platform }}-${{ env.BUILD_CONFIGURATION }} path: artifacts/VDD/${{ matrix.platform }}/ if-no-files-found: error + + - name: Upload ARM64 attestation CAB + if: matrix.platform == 'ARM64' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: VDD-ARM64-Attestation-CAB + path: artifacts/VDD/ARM64/MttVDD-ARM64-Attestation.cab + if-no-files-found: error diff --git a/docs/windows-arm64-signing.md b/docs/windows-arm64-signing.md new file mode 100644 index 0000000..b18ed1f --- /dev/null +++ b/docs/windows-arm64-signing.md @@ -0,0 +1,121 @@ +# Windows ARM64 signing and maintainer handoff + +This runbook describes how Virtual Display Driver maintainers can produce a **Microsoft-signed ARM64 preview** that installs on HVCI-enabled Windows on ARM systems (for example Surface Laptop 7 with Snapdragon X), and how to graduate that package to WHCP certification later. + +## Background + +- RealWarp and other consumers detect `MttVDD` through `Root\MttVDD` and `\\.\pipe\MTTVirtualDisplayPipe`. +- The upstream ARM64 release payload is architecturally correct (`NTARM64`, native `MttVDD.dll`), but SignPath/GlobalSign catalogs are rejected by the Driver Store on systems with Memory Integrity enabled (`0x800B0109` / untrusted root). +- `PnpLockdown=1` in `MttVDD.inf` is **installed-file protection** and should remain enabled; it is not a WHQL-only switch. + +Related issues: + +- [#465](https://github.com/VirtualDrivers/Virtual-Display-Driver/issues/465) — ARM64 package trust failure on install +- [#483](https://github.com/VirtualDrivers/Virtual-Display-Driver/issues/483) — post-install display attachment on ARM + +## Repository automation + +| Script | Purpose | +| --- | --- | +| `scripts/Test-Arm64DriverPackage.ps1` | Validates folder or CAB payloads (`CI` or `Release` policy) | +| `scripts/New-AttestationCab.ps1` | Builds a Partner Center-ready attestation CAB from ARM64 Release output | +| `scripts/Invoke-Arm64AcceptanceTest.ps1` | Surface acceptance harness (Secure Boot/HVCI + Release signature gate + optional install) | + +GitHub Actions (`.github/workflows/ci-validation.yml`) builds ARM64 Release output, runs **CI** validation, creates the attestation CAB, round-trip validates the CAB, and uploads: + +- `VDD-ARM64-Release` — raw build output +- `VDD-ARM64-Attestation-CAB` — submission CAB (unsigned; EV signing happens offline) + +## Maintainer workflow + +### 1. Build and validate locally or from CI + +```powershell +# After ARM64 Release build output exists: +./scripts/Test-Arm64DriverPackage.ps1 ` + -PackagePath "Virtual Display Driver (HDR)\ARM64\Release\MttVDD" ` + -Policy CI + +./scripts/New-AttestationCab.ps1 ` + -InputDirectory "Virtual Display Driver (HDR)\ARM64\Release\MttVDD" ` + -OutputDirectory "artifacts\VDD\ARM64" +``` + +Expected payload inside the CAB subfolder: + +- `MttVDD.inf` +- `MttVDD.dll` (PE machine `0xAA64`) +- `MttVDD.pdb` +- `MttVDD.cat` (company catalog for verification; Microsoft replaces it) + +Requirements from Microsoft attestation signing: + +- Driver files must live in a **single subfolder** inside the CAB (never at CAB root). +- Subfolder name must be **fewer than 40 characters** and use no UNC paths during packaging. +- See [Attestation sign Windows drivers](https://learn.microsoft.com/windows-hardware/drivers/dashboard/code-signing-attestation). + +### 2. EV-sign the CAB offline + +Use the organization EV certificate and SignTool (SHA-256): + +```cmd +SignTool sign /s MY /n "Company Name" /fd sha256 /tr http://timestamp.digicert.com /td sha256 /v MttVDD-ARM64-Attestation.cab +``` + +Do **not** store EV credentials in GitHub Actions. + +### 3. Submit attestation preview in Partner Center + +1. Open the [Partner Center hardware dashboard](https://partner.microsoft.com/dashboard/hardware/Search). +2. Choose **Submit new hardware**. +3. Upload the **EV-signed CAB**. +4. Leave test-signing options **unchecked** for the production-preview path on HVCI systems. +5. Request the Windows Desktop signatures needed for ARM64 user-mode driver attestation. +6. Download Microsoft’s returned package when processing completes. + +Reference: [Driver signing options and best practices — attestation for testing scenarios](https://learn.microsoft.com/windows-hardware/drivers/dashboard/driver-signing-offerings#attestation-signed-drivers-for-testing-scenarios). + +### 4. Validate the returned package (Release policy) + +```powershell +./scripts/Test-Arm64DriverPackage.ps1 ` + -PackagePath ".\downloads\MttVDD-ARM64-MicrosoftSigned.cab" ` + -Policy Release +``` + +Release policy requires a catalog subject containing **Microsoft Windows Hardware Compatibility Publisher** and rejects SignPath-only catalogs. + +Optional Surface preflight: + +```powershell +./scripts/Invoke-Arm64AcceptanceTest.ps1 ` + -PackagePath ".\downloads\MttVDD-ARM64-MicrosoftSigned.cab" ` + -Mode Preflight +``` + +### 5. Publish an ARM64 preview release + +1. Extract the Microsoft-signed package into a clearly named release asset, for example `VirtualDisplayDriver-ARM64.Driver.Only.zip`. +2. Mark the release as **ARM64 preview / attestation-signed** in release notes. +3. Link issues #465 and #483 and note that WHCP certification is the follow-up for broad retail/Windows Update distribution. + +### 6. Graduate to WHCP (later) + +Reuse the same validated ARM64 payload for HLK/WHCP submission when an ARM64 HLK lab is available. Attestation preview unblocks HVCI-enabled test machines; WHCP remains the supported public-release path. + +## Acceptance criteria on Windows on ARM + +Run with Secure Boot and Memory Integrity **enabled** (no test-signing boot configuration or trust-store workarounds): + +1. **Static gate** — `Test-Arm64DriverPackage.ps1 -Policy Release` passes. +2. **Install gate** — `Invoke-Arm64AcceptanceTest.ps1 -Mode Install` installs `Root\MttVDD` with device status OK and no Code Integrity rejection. +3. **Display gate** — one free-tier virtual display attaches to extended topology (DisplayConfig / `EnumDisplayMonitors` / DXGI). +4. **RealWarp gate** — RealWarp 0.38.0 (x64 emulation) detects MttVDD, connects to `MTTVirtualDisplayPipe`, captures desktop, and renders on XREAL One / One Pro. + +If signing succeeds but topology fails, treat that as issue #483 (IddCx attachment) rather than changing signing policy. + +## Evidence captured on Surface (SignPath baseline) + +The current public ARM64 package (`25.7.23`) fails Release validation because the catalog chains to **SignPath Foundation / GlobalSign**, not Microsoft WHCP. On HVCI-enabled systems this surfaces as Driver Store staging error **`0x800B0109`** (*A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider*). + +That failure occurs **before** UMDF/IddCx load; replacing RealWarp’s bundled x64 Parsec driver with a **Microsoft-signed ARM64 MttVDD** package is the required production path. diff --git a/scripts/Invoke-Arm64AcceptanceTest.ps1 b/scripts/Invoke-Arm64AcceptanceTest.ps1 new file mode 100644 index 0000000..41b6e05 --- /dev/null +++ b/scripts/Invoke-Arm64AcceptanceTest.ps1 @@ -0,0 +1,202 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Runs Surface acceptance checks for a Microsoft-signed ARM64 MttVDD package. + +.DESCRIPTION + Preflight mode verifies Secure Boot and HVCI remain enabled and validates the package + with Release policy. Install mode performs NefCon-based installation and display + topology checks when Release validation succeeds. + +.PARAMETER PackagePath + Directory or CAB containing the Microsoft-signed ARM64 driver package. + +.PARAMETER Mode + Preflight - security and signature validation only (default) + Install - install driver and verify device/display topology when signature gate passes + +.PARAMETER NefConPath + Optional path to nefconw.exe. Defaults to ARM64 binary extracted from the latest NefCon release. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$PackagePath, + + [ValidateSet('Preflight', 'Install')] + [string]$Mode = 'Preflight', + + [string]$NefConPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Test-SecurityGates { + $results = @() + + $secureBoot = Confirm-SecureBootUEFI -ErrorAction SilentlyContinue + $results += [pscustomobject]@{ + Check = 'SecureBoot' + Status = $(if ($secureBoot) { 'PASS' } else { 'FAIL' }) + Detail = $(if ($secureBoot) { 'Secure Boot is enabled.' } else { 'Secure Boot is disabled or unavailable.' }) + } + + $hvciEnabled = $false + try { + $ci = Get-CimInstance -ClassName Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard -ErrorAction Stop + $hvciEnabled = ($ci.SecurityServicesRunning -contains 1) -or ($ci.VirtualizationBasedSecurityStatus -ge 2) + } + catch { + $reg = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\DeviceGuard\Scenarios\HypervisorEnforcedCodeIntegrity' -Name Enabled -ErrorAction SilentlyContinue + $hvciEnabled = ($reg.Enabled -eq 1) + } + + $results += [pscustomobject]@{ + Check = 'HVCI' + Status = $(if ($hvciEnabled) { 'PASS' } else { 'FAIL' }) + Detail = $(if ($hvciEnabled) { 'Memory Integrity / HVCI appears enabled.' } else { 'Memory Integrity / HVCI does not appear enabled.' }) + } + + return $results +} + +function Get-DriverPackageDirectory { + param( + [string]$Path + ) + + $item = Get-Item -LiteralPath $Path + if ($item.PSIsContainer) { + $inf = Get-ChildItem -LiteralPath $item.FullName -Filter 'MttVDD.inf' -Recurse -File | Select-Object -First 1 + if (-not $inf) { + throw "MttVDD.inf not found under $Path" + } + return $inf.Directory.FullName + } + + $expandRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("vdd-arm64-accept-" + [guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $expandRoot -Force | Out-Null + expand.exe $item.FullName -F:* $expandRoot | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Failed to expand CAB: $Path" + } + + $inf = Get-ChildItem -LiteralPath $expandRoot -Filter 'MttVDD.inf' -Recurse -File | Select-Object -First 1 + if (-not $inf) { + throw "MttVDD.inf not found in expanded CAB." + } + + return $inf.Directory.FullName +} + +function Ensure-NefCon { + param( + [string]$ExplicitPath + ) + + if ($ExplicitPath) { + if (-not (Test-Path -LiteralPath $ExplicitPath)) { + throw "NefCon not found at $ExplicitPath" + } + return (Resolve-Path -LiteralPath $ExplicitPath).Path + } + + $tempDir = Join-Path $env:TEMP 'VDDAcceptanceNefCon' + $arm64Exe = Join-Path $tempDir 'ARM64\nefconw.exe' + if (-not (Test-Path -LiteralPath $arm64Exe)) { + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + $zipPath = Join-Path $tempDir 'nefcon.zip' + Invoke-WebRequest -Uri 'https://github.com/nefarius/nefcon/releases/download/v1.14.0/nefcon_v1.14.0.zip' -OutFile $zipPath -UseBasicParsing + Expand-Archive -LiteralPath $zipPath -DestinationPath $tempDir -Force + } + + if (-not (Test-Path -LiteralPath $arm64Exe)) { + throw 'ARM64 nefconw.exe not found after extraction.' + } + + return (Resolve-Path -LiteralPath $arm64Exe).Path +} + +$validator = Join-Path $PSScriptRoot 'Test-Arm64DriverPackage.ps1' +if (-not (Test-Path -LiteralPath $validator)) { + throw "Validation script not found: $validator" +} + +Write-Host '=== ARM64 MttVDD acceptance preflight ===' -ForegroundColor Cyan +$securityResults = Test-SecurityGates +$securityResults | ForEach-Object { + $color = if ($_.Status -eq 'PASS') { 'Green' } else { 'Red' } + Write-Host ("[{0}] {1} - {2}" -f $_.Status, $_.Check, $_.Detail) -ForegroundColor $color +} + +if (@($securityResults | Where-Object { $_.Status -eq 'FAIL' }).Count -gt 0) { + throw 'Security gate failed. Acceptance testing requires Secure Boot and HVCI enabled.' +} + +Write-Host '=== Release signature validation ===' -ForegroundColor Cyan +& $validator -PackagePath $PackagePath -Policy Release + +if ($Mode -eq 'Preflight') { + Write-Host 'Preflight acceptance checks passed. Install mode can proceed once a Microsoft-signed package is available.' -ForegroundColor Green + return +} + +$packageDir = Get-DriverPackageDirectory -Path $PackagePath +$nefcon = Ensure-NefCon -ExplicitPath $NefConPath +$infPath = Join-Path $packageDir 'MttVDD.inf' + +Write-Host '=== Installing Root\MttVDD via NefCon ===' -ForegroundColor Cyan +Push-Location $packageDir +try { + & $nefcon install $infPath 'Root\MttVDD' + if ($LASTEXITCODE -ne 0) { + throw "nefconw install failed with exit code $LASTEXITCODE" + } +} +finally { + Pop-Location +} + +Start-Sleep -Seconds 5 + +$pnpDevice = Get-PnpDevice -FriendlyName '*Virtual Display Driver*' -ErrorAction SilentlyContinue | Select-Object -First 1 +if (-not $pnpDevice) { + $pnpDevice = Get-PnpDevice -InstanceId '*Root\MttVDD*' -ErrorAction SilentlyContinue | Select-Object -First 1 +} + +if (-not $pnpDevice -or $pnpDevice.Status -ne 'OK') { + throw 'MttVDD device not present or not healthy after install.' +} + +Write-Host ("Device {0} status: {1}" -f $pnpDevice.InstanceId, $pnpDevice.Status) -ForegroundColor Green + +try { + Add-Type @" +using System; +using System.Runtime.InteropServices; +public static class MonitorEnumAcceptance { + public delegate bool EnumMonitorsDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData); + [StructLayout(LayoutKind.Sequential)] + public struct RECT { public int Left; public int Top; public int Right; public int Bottom; } + [DllImport("user32.dll")] + public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, EnumMonitorsDelegate lpfnEnum, IntPtr dwData); + public static int CountMonitors() { + int count = 0; + EnumMonitorsDelegate callback = delegate (IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData) { + count++; + return true; + }; + EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, callback, IntPtr.Zero); + return count; + } +} +"@ + $monitorCount = [MonitorEnumAcceptance]::CountMonitors() + Write-Host "EnumDisplayMonitors reported $monitorCount monitors." -ForegroundColor Green +} +catch { + Write-Warning "EnumDisplayMonitors probe unavailable: $($_.Exception.Message)" +} +Write-Host 'Install acceptance checks completed. Verify extended topology manually and test RealWarp 0.38.0 with XREAL One / One Pro.' -ForegroundColor Green diff --git a/scripts/New-AttestationCab.ps1 b/scripts/New-AttestationCab.ps1 new file mode 100644 index 0000000..2c4a0f6 --- /dev/null +++ b/scripts/New-AttestationCab.ps1 @@ -0,0 +1,164 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Creates a Partner Center-ready attestation CAB from ARM64 Release build output. + +.DESCRIPTION + Validates the ARM64 Release payload with CI policy, stages the driver into a short-path + directory, and invokes MakeCab to produce a Microsoft-compatible submission CAB. + +.PARAMETER InputDirectory + ARM64 Release output directory containing MttVDD.inf, MttVDD.dll, MttVDD.pdb, and catalog. + +.PARAMETER OutputDirectory + Directory where MakeCab Disk1 output will be copied. Defaults to InputDirectory parent. + +.PARAMETER PackageFolderName + Single driver subfolder name inside the CAB. Must be fewer than 40 characters. + +.PARAMETER CabFileName + Output CAB file name. + +.PARAMETER StagingRoot + Optional short local path for MakeCab staging. Must not be a UNC path. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$InputDirectory, + + [string]$OutputDirectory, + + [ValidateLength(1, 39)] + [string]$PackageFolderName = 'MttVDD', + + [string]$CabFileName = 'MttVDD-ARM64-Attestation.cab', + + [string]$StagingRoot = 'C:\VDDSubmit' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Resolve-ExistingPath { + param( + [string]$Path, + [string]$Label + ) + + if (-not (Test-Path -LiteralPath $Path)) { + throw "$Label not found: $Path" + } + + return (Resolve-Path -LiteralPath $Path).Path +} + +function Get-CatalogPath { + param( + [string]$Directory + ) + + foreach ($name in @('MttVDD.cat', 'mttvdd.cat')) { + $candidate = Join-Path $Directory $name + if (Test-Path -LiteralPath $candidate) { + return (Resolve-Path -LiteralPath $candidate).Path + } + } + + throw "Catalog file not found in $Directory" +} + +$validator = Join-Path $PSScriptRoot 'Test-Arm64DriverPackage.ps1' +if (-not (Test-Path -LiteralPath $validator)) { + throw "Validation script not found: $validator" +} + +$inputDir = Resolve-ExistingPath -Path $InputDirectory -Label 'InputDirectory' +& $validator -PackagePath $inputDir -Policy CI + +if ($StagingRoot -match '^\\\\') { + throw 'StagingRoot must be a mapped drive or local path; UNC paths are rejected by Partner Center attestation packaging.' +} + +$requiredFiles = @('MttVDD.inf', 'MttVDD.dll', 'MttVDD.pdb') +foreach ($fileName in $requiredFiles) { + Resolve-ExistingPath -Path (Join-Path $inputDir $fileName) -Label $fileName | Out-Null +} +$catalogPath = Get-CatalogPath -Directory $inputDir + +if (-not $OutputDirectory) { + $OutputDirectory = Split-Path -Parent $inputDir +} +$OutputDirectory = Resolve-ExistingPath -Path $OutputDirectory -Label 'OutputDirectory' + +$stagingSession = Join-Path $StagingRoot ([guid]::NewGuid().ToString('N')) +$stagingPackage = Join-Path $stagingSession $PackageFolderName +New-Item -ItemType Directory -Path $stagingPackage -Force | Out-Null + +try { + Copy-Item -LiteralPath (Join-Path $inputDir 'MttVDD.inf') -Destination $stagingPackage -Force + Copy-Item -LiteralPath (Join-Path $inputDir 'MttVDD.dll') -Destination $stagingPackage -Force + Copy-Item -LiteralPath (Join-Path $inputDir 'MttVDD.pdb') -Destination $stagingPackage -Force + Copy-Item -LiteralPath $catalogPath -Destination (Join-Path $stagingPackage ([System.IO.Path]::GetFileName($catalogPath))) -Force + + $ddfPath = Join-Path $stagingSession 'MttVDD-ARM64.ddf' + $cabTemplate = [System.IO.Path]::GetFileNameWithoutExtension($CabFileName) + $ddfLines = @( + '; MttVDD ARM64 attestation submission' + '.OPTION EXPLICIT' + '.Set CabinetFileCountThreshold=0' + '.Set FolderFileCountThreshold=0' + '.Set FolderSizeThreshold=0' + '.Set MaxCabinetSize=0' + '.Set MaxDiskFileCount=0' + '.Set MaxDiskSize=0' + '.Set CompressionType=MSZIP' + '.Set Cabinet=on' + '.Set Compress=on' + ".Set CabinetNameTemplate=$cabTemplate.cab" + ".Set DestinationDir=$PackageFolderName" + "$(Join-Path $stagingPackage 'MttVDD.inf')" + "$(Join-Path $stagingPackage 'MttVDD.dll')" + "$(Join-Path $stagingPackage 'MttVDD.pdb')" + "$(Join-Path $stagingPackage ([System.IO.Path]::GetFileName($catalogPath)))" + ) + Set-Content -LiteralPath $ddfPath -Value $ddfLines -Encoding ASCII + + Push-Location $stagingSession + try { + & makecab.exe /F $ddfPath + if ($LASTEXITCODE -ne 0) { + throw "makecab.exe failed with exit code $LASTEXITCODE" + } + } + finally { + Pop-Location + } + + $disk1 = Join-Path $stagingSession 'disk1' + $builtCab = Join-Path $disk1 ($cabTemplate + '.cab') + if (-not (Test-Path -LiteralPath $builtCab)) { + $builtCab = Get-ChildItem -LiteralPath $disk1 -Filter '*.cab' -File | Select-Object -First 1 -ExpandProperty FullName + } + if (-not $builtCab -or -not (Test-Path -LiteralPath $builtCab)) { + throw 'MakeCab completed but no CAB file was produced.' + } + + $finalCab = Join-Path $OutputDirectory $CabFileName + Copy-Item -LiteralPath $builtCab -Destination $finalCab -Force + + & $validator -PackagePath $finalCab -Policy CI + + [pscustomobject]@{ + CabPath = (Resolve-Path -LiteralPath $finalCab).Path + PackageFolderName = $PackageFolderName + StagingDirectory = $stagingSession + SourceDirectory = $inputDir + } +} +finally { + if (Test-Path -LiteralPath $stagingSession) { + Remove-Item -LiteralPath $stagingSession -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/scripts/Test-Arm64DriverPackage.ps1 b/scripts/Test-Arm64DriverPackage.ps1 new file mode 100644 index 0000000..067861b --- /dev/null +++ b/scripts/Test-Arm64DriverPackage.ps1 @@ -0,0 +1,383 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Validates an ARM64 MttVDD driver package directory or attestation CAB. + +.DESCRIPTION + CI policy validates architecture, required payload files, INF integrity, PE machine + type, and version consistency. Release policy additionally requires a catalog that + chains to Microsoft Windows Hardware Compatibility Publisher and rejects SignPath-only + or test-signed catalogs. + +.PARAMETER PackagePath + Path to a driver package folder or a .cab file produced for Partner Center submission. + +.PARAMETER Policy + CI - pre-submission validation (default) + Release - post-Microsoft-signing validation before publication or install +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$PackagePath, + + [ValidateSet('CI', 'Release')] + [string]$Policy = 'CI', + + [switch]$KeepExpandedCab +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$script:Arm64MachineType = 0xAA64 +$script:MicrosoftHardwarePublisher = 'Microsoft Windows Hardware Compatibility Publisher' +$script:RejectedSignerPatterns = @( + 'SignPath Foundation', + 'SignPath Test' +) +$script:RequiredInfMarkers = @( + 'PnpLockdown=1', + 'CatalogFile=MttVDD.cat', + 'UmdfExtensions = IddCx0102', + 'Root\MttVDD' +) +$script:RequiredPackageFiles = @( + 'MttVDD.inf', + 'MttVDD.dll', + 'MttVDD.pdb' +) +$script:RequiredCatalogNames = @( + 'MttVDD.cat', + 'mttvdd.cat' +) + +function Write-ValidationResult { + param( + [string]$Name, + [bool]$Passed, + [string]$Detail + ) + + $status = if ($Passed) { 'PASS' } else { 'FAIL' } + [pscustomobject]@{ + Check = $Name + Status = $status + Detail = $Detail + } +} + +function Resolve-PackageDirectory { + param( + [string]$Path, + [switch]$KeepExpanded + ) + + if (-not (Test-Path -LiteralPath $Path)) { + throw "Package path not found: $Path" + } + + $item = Get-Item -LiteralPath $Path + if ($item.PSIsContainer) { + return @{ + Directory = $item.FullName + ExpandedRoot = $null + Cleanup = $false + } + } + + if ($item.Extension -ne '.cab') { + throw "PackagePath must be a directory or .cab file. Received: $Path" + } + + $expandRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("vdd-arm64-cab-" + [guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $expandRoot -Force | Out-Null + + try { + expand.exe $item.FullName -F:* $expandRoot | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "expand.exe failed with exit code $LASTEXITCODE while extracting $($item.FullName)" + } + } + catch { + Remove-Item -LiteralPath $expandRoot -Recurse -Force -ErrorAction SilentlyContinue + throw + } + + if (-not $KeepExpanded) { + return @{ + Directory = $expandRoot + ExpandedRoot = $expandRoot + Cleanup = $true + } + } + + return @{ + Directory = $expandRoot + ExpandedRoot = $null + Cleanup = $false + } +} + +function Get-DriverPackageRoot { + param( + [string]$RootDirectory + ) + + $infFiles = @(Get-ChildItem -LiteralPath $RootDirectory -Filter '*.inf' -Recurse -File -ErrorAction SilentlyContinue) + if ($infFiles.Count -eq 0) { + throw "No .inf files found under package root: $RootDirectory" + } + + $mttInf = $infFiles | Where-Object { $_.Name -ieq 'MttVDD.inf' } | Select-Object -First 1 + if (-not $mttInf) { + throw "Expected MttVDD.inf in package, found: $($infFiles.Name -join ', ')" + } + + return @{ + PackageDirectory = $mttInf.Directory.FullName + InfPath = $mttInf.FullName + } +} + +function Test-CabLayout { + param( + [string]$RootDirectory + ) + + $rootEntries = @(Get-ChildItem -LiteralPath $RootDirectory -Force) + $rootFiles = @($rootEntries | Where-Object { -not $_.PSIsContainer }) + if ($rootFiles.Count -gt 0) { + return Write-ValidationResult -Name 'CabLayout' -Passed $false -Detail 'CAB must not contain files at the root; driver payload must live in a subfolder.' + } + + $packageDirs = @($rootEntries | Where-Object { $_.PSIsContainer }) + if ($packageDirs.Count -ne 1) { + return Write-ValidationResult -Name 'CabLayout' -Passed $false -Detail "Expected exactly one driver subfolder in CAB, found $($packageDirs.Count)." + } + + if ($packageDirs[0].Name.Length -ge 40) { + return Write-ValidationResult -Name 'CabLayout' -Passed $false -Detail "Driver folder name must be fewer than 40 characters for attestation submission: $($packageDirs[0].Name)" + } + + Write-ValidationResult -Name 'CabLayout' -Passed $true -Detail "Single driver folder '$($packageDirs[0].Name)' present." +} + +function Get-PeMachineType { + param( + [string]$BinaryPath + ) + + $bytes = [System.IO.File]::ReadAllBytes($BinaryPath) + if ($bytes.Length -lt 0x40) { + throw "Binary too small to parse PE header: $BinaryPath" + } + + $peOffset = [BitConverter]::ToInt32($bytes, 0x3C) + if ($peOffset -lt 0 -or ($peOffset + 6) -ge $bytes.Length) { + throw "Invalid PE offset in binary: $BinaryPath" + } + + return [BitConverter]::ToUInt16($bytes, $peOffset + 4) +} + +function Get-InfDriverVersion { + param( + [string]$InfPath + ) + + $driverVerLine = Select-String -LiteralPath $InfPath -Pattern '^\s*DriverVer\s*=' -SimpleMatch:$false | Select-Object -First 1 + if (-not $driverVerLine) { + return $null + } + + $value = ($driverVerLine.Line -split '=', 2)[1].Trim() + if ([string]::IsNullOrWhiteSpace($value)) { + return $null + } + + return $value +} + +function Test-InfContent { + param( + [string]$InfPath + ) + + $infText = Get-Content -LiteralPath $InfPath -Raw + $results = @() + + foreach ($marker in $script:RequiredInfMarkers) { + $passed = $infText -like "*$marker*" + $results += Write-ValidationResult -Name "InfMarker:$marker" -Passed $passed -Detail $(if ($passed) { 'Present.' } else { 'Missing required INF marker.' }) + } + + $hasNtArm64 = ($infText -match '\[Standard\.NTARM64\]' -or $infText -match 'NTARM64') + $hasAmd64Only = ($infText -match '\[Standard\.NTamd64\]' -or $infText -match ',NTamd64' -or $infText -match 'NTamd64\.10\.0') + $archPassed = $hasNtArm64 -and -not $hasAmd64Only + $results += Write-ValidationResult -Name 'InfArchitecture' -Passed $archPassed -Detail $(if ($archPassed) { 'INF targets NTARM64.' } else { 'INF must target NTARM64 and must not be amd64-only.' }) + + return $results +} + +function Test-RequiredFiles { + param( + [string]$PackageDirectory + ) + + $results = @() + foreach ($fileName in $script:RequiredPackageFiles) { + $path = Join-Path $PackageDirectory $fileName + $passed = Test-Path -LiteralPath $path + $results += Write-ValidationResult -Name "File:$fileName" -Passed $passed -Detail $(if ($passed) { 'Present.' } else { 'Missing required package file.' }) + } + + $catalogPath = $null + foreach ($catalogName in $script:RequiredCatalogNames) { + $candidate = Join-Path $PackageDirectory $catalogName + if (Test-Path -LiteralPath $candidate) { + $catalogPath = $candidate + break + } + } + + $catalogPassed = [bool]$catalogPath + $results += Write-ValidationResult -Name 'File:Catalog' -Passed $catalogPassed -Detail $(if ($catalogPassed) { "Present ($([System.IO.Path]::GetFileName($catalogPath)))." } else { 'Missing MttVDD.cat catalog file.' }) + + return @{ + Results = $results + CatalogPath = $catalogPath + } +} + +function Test-PeArchitecture { + param( + [string]$DllPath + ) + + $machine = Get-PeMachineType -BinaryPath $DllPath + $passed = ($machine -eq $script:Arm64MachineType) + Write-ValidationResult -Name 'PeMachineType' -Passed $passed -Detail "Expected 0x$('{0:X4}' -f $script:Arm64MachineType), found 0x$('{0:X4}' -f $machine)." +} + +function Test-VersionConsistency { + param( + [string]$InfPath, + [string]$DllPath + ) + + $driverVer = Get-InfDriverVersion -InfPath $InfPath + if (-not $driverVer) { + return Write-ValidationResult -Name 'VersionConsistency' -Passed $false -Detail 'DriverVer is missing from INF.' + } + + $fileVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($DllPath) + $dllVersion = $fileVersion.ProductVersion + if ([string]::IsNullOrWhiteSpace($dllVersion)) { + $dllVersion = $fileVersion.FileVersion + } + if (-not [string]::IsNullOrWhiteSpace($dllVersion)) { + $dllVersion = $dllVersion.Trim() + } + + if ([string]::IsNullOrWhiteSpace($dllVersion) -or $dllVersion -eq '0.0.0.0') { + return Write-ValidationResult -Name 'VersionConsistency' -Passed $true -Detail "INF DriverVer='$driverVer'; DLL version resource not stamped (acceptable when INF/catalog carry the release version)." + } + + $passed = ($driverVer -match [regex]::Escape($dllVersion)) + Write-ValidationResult -Name 'VersionConsistency' -Passed $passed -Detail "INF DriverVer='$driverVer', DLL version='$dllVersion'." +} + +function Get-CatalogCertificateSubjects { + param( + [string]$CatalogPath + ) + + $certificates = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection + $certificates.Import([System.IO.File]::ReadAllBytes($CatalogPath)) + return @($certificates | ForEach-Object { $_.Subject }) +} + +function Test-CatalogSignaturePolicy { + param( + [string]$CatalogPath, + [string]$PolicyName + ) + + if (-not $CatalogPath) { + return Write-ValidationResult -Name 'CatalogSignature' -Passed $false -Detail 'Catalog file not available for signature analysis.' + } + + $subjects = @(Get-CatalogCertificateSubjects -CatalogPath $CatalogPath) + if ($subjects.Count -eq 0) { + return Write-ValidationResult -Name 'CatalogSignature' -Passed $false -Detail 'No certificates embedded in catalog.' + } + + $subjectText = ($subjects -join ' | ') + $hasMicrosoftPublisher = $subjects | Where-Object { $_ -like "*$($script:MicrosoftHardwarePublisher)*" } + $hasRejectedSigner = $false + foreach ($pattern in $script:RejectedSignerPatterns) { + if ($subjects | Where-Object { $_ -like "*$pattern*" }) { + $hasRejectedSigner = $true + break + } + } + + if ($PolicyName -eq 'Release') { + $passed = [bool]$hasMicrosoftPublisher -and -not $hasRejectedSigner + $detail = if ($passed) { + 'Catalog chains to Microsoft Windows Hardware Compatibility Publisher.' + } + elseif ($hasRejectedSigner -and -not $hasMicrosoftPublisher) { + 'Catalog is SignPath/test-signed only; Microsoft hardware publisher signature required for HVCI-enabled install.' + } + else { + "Catalog subjects: $subjectText" + } + return Write-ValidationResult -Name 'CatalogSignature' -Passed $passed -Detail $detail + } + + return Write-ValidationResult -Name 'CatalogSignature' -Passed $true -Detail "CI policy records catalog subjects: $subjectText" +} + +$expanded = Resolve-PackageDirectory -Path $PackagePath -KeepExpanded:$KeepExpandedCab +$results = @() + +try { + if ((Get-Item -LiteralPath $PackagePath).Extension -eq '.cab') { + $results += Test-CabLayout -RootDirectory $expanded.Directory + } + + $package = Get-DriverPackageRoot -RootDirectory $expanded.Directory + $fileResults = Test-RequiredFiles -PackageDirectory $package.PackageDirectory + $results += $fileResults.Results + + $results += Test-InfContent -InfPath $package.InfPath + + $dllPath = Join-Path $package.PackageDirectory 'MttVDD.dll' + if (Test-Path -LiteralPath $dllPath) { + $results += Test-PeArchitecture -DllPath $dllPath + $results += Test-VersionConsistency -InfPath $package.InfPath -DllPath $dllPath + } + + $results += Test-CatalogSignaturePolicy -CatalogPath $fileResults.CatalogPath -PolicyName $Policy + + $failed = @($results | Where-Object { $_.Status -eq 'FAIL' }) + $results | Format-Table -AutoSize | Out-String | Write-Verbose + $results | ForEach-Object { + $color = if ($_.Status -eq 'PASS') { 'Green' } else { 'Red' } + Write-Host ("[{0}] {1} - {2}" -f $_.Status, $_.Check, $_.Detail) -ForegroundColor $color + } + + if ($failed.Count -gt 0) { + throw "ARM64 driver package validation failed ($Policy policy): $($failed.Check -join ', ')" + } + + Write-Host "ARM64 driver package validation succeeded ($Policy policy)." -ForegroundColor Green +} +finally { + if ($expanded.Cleanup -and $expanded.ExpandedRoot) { + Remove-Item -LiteralPath $expanded.ExpandedRoot -Recurse -Force -ErrorAction SilentlyContinue + } +} From 0149a01e1c93e45af99bcc6ba56ac4b017a87993 Mon Sep 17 00:00:00 2001 From: Kris Pennington Date: Wed, 2 Sep 2026 10:27:47 -0500 Subject: [PATCH 2/2] Fix Secure Boot preflight when Confirm-SecureBootUEFI needs elevation Co-authored-by: Cursor --- scripts/Invoke-Arm64AcceptanceTest.ps1 | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/scripts/Invoke-Arm64AcceptanceTest.ps1 b/scripts/Invoke-Arm64AcceptanceTest.ps1 index 41b6e05..eabe219 100644 --- a/scripts/Invoke-Arm64AcceptanceTest.ps1 +++ b/scripts/Invoke-Arm64AcceptanceTest.ps1 @@ -36,11 +36,19 @@ $ErrorActionPreference = 'Stop' function Test-SecurityGates { $results = @() - $secureBoot = Confirm-SecureBootUEFI -ErrorAction SilentlyContinue + $secureBoot = $false + try { + $secureBoot = Confirm-SecureBootUEFI -ErrorAction Stop + } + catch { + $reg = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\State' -Name UEFISecureBootEnabled -ErrorAction SilentlyContinue + $secureBoot = ($reg.UEFISecureBootEnabled -eq 1) + } + $results += [pscustomobject]@{ Check = 'SecureBoot' Status = $(if ($secureBoot) { 'PASS' } else { 'FAIL' }) - Detail = $(if ($secureBoot) { 'Secure Boot is enabled.' } else { 'Secure Boot is disabled or unavailable.' }) + Detail = $(if ($secureBoot) { 'Secure Boot is enabled.' } else { 'Secure Boot is disabled or could not be verified.' }) } $hvciEnabled = $false