From 809061d99a6bc916c0df0a1253e7a784dae5053f Mon Sep 17 00:00:00 2001 From: Artur Khantimirov Date: Fri, 23 Oct 2026 14:37:26 +1300 Subject: [PATCH] test: add offline eMudhra host trust checks Check Linux CA bundles and Windows installed/cached roots before accepting a VHD. Require a positive control and fail on the two signing roots or unreadable trust data. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 685d2bf4-2f4b-4b28-8841-a476cf1672ef --- .../linux_vhd_content_test_helpers_spec.sh | 28 ++++++++++++++++ vhdbuilder/packer/README.md | 8 +++++ .../packer/test/linux-vhd-content-test.sh | 20 ++++++++++++ .../packer/test/windows-vhd-content-test.ps1 | 16 ++++++++++ .../test/windows-vhd-content-test.tests.ps1 | 32 +++++++++++++++++++ 5 files changed, 104 insertions(+) diff --git a/spec/vhdbuilder/packer/test/linux_vhd_content_test_helpers_spec.sh b/spec/vhdbuilder/packer/test/linux_vhd_content_test_helpers_spec.sh index 8d0d025b5cf..ea5263575ea 100644 --- a/spec/vhdbuilder/packer/test/linux_vhd_content_test_helpers_spec.sh +++ b/spec/vhdbuilder/packer/test/linux_vhd_content_test_helpers_spec.sh @@ -3,6 +3,34 @@ # ShellSpec tests for content-test helpers and call sites +Describe 'eMudhra root removal' + BeforeAll "eval \"\$(sed -n '/^testEmudhraRootRemoval()/,/^}/p' vhdbuilder/packer/test/linux-vhd-content-test.sh)\"" + err() { echo "$1:Error: $2" >&2; } + openssl() { + if [ "$1" = crl2pkcs7 ]; then + printf 'subject=CN = %s\n' "$CONTROL" "$ROOT" + return "$READ_STATUS" + fi + cat + } + Parameters + 'DigiCert Global Root G2' '' 0 /dev/null 0 + 'DigiCert Global Root G2' 'emSign ECC Root CA - G3' 0 /dev/null 1 + 'DigiCert Global Root G2' 'AAA Certificate Services' 0 /dev/null 1 + '' '' 0 /dev/null 1 + 'DigiCert Global Root G2' '' 1 /dev/null 1 + 'DigiCert Global Root G2' '' 0 "${SHELLSPEC_WORKDIR}/absent-ca-bundle" 1 + End + It "checks root '$2', read status $3 and bundle $4" + CONTROL="$1" ROOT="$2" READ_STATUS="$3" + When call testEmudhraRootRemoval "$4" + The status should equal "$5" + if [ "$5" -ne 0 ]; then + The stderr should include 'testEmudhraRootRemoval:Error:' + fi + End +End + Describe 'Linux content-test legacy parameters' read_legacy_parameters() { eval "$(sed -n '/^OS_VERSION=/,/^AGENTBAKER_REPOSITORY_URL=/p' vhdbuilder/packer/test/linux-vhd-content-test.sh)" diff --git a/vhdbuilder/packer/README.md b/vhdbuilder/packer/README.md index dc5523e20be..a0302f1b3a0 100644 --- a/vhdbuilder/packer/README.md +++ b/vhdbuilder/packer/README.md @@ -25,6 +25,14 @@ script, and build the target-architecture binaries. The following VHD step runs in the same job and reuses those files. For local builds, run `build-tools` before `run-packer` or `run-imagecustomizer`. +# eMudhra host OS checks + +The offline gate requires removal of emSign ECC Root CA - G3 and AAA Certificate +Services from Linux CA bundles and Windows installed/cached root lists. DigiCert +Global Root G2 is the read-control. This is a conservative root-removal policy, +not full chain verification: images protected by intermediate blocklists or +revocation can still fail. No component, container, or custom CA-store checks. + # Ubuntu BCC build Ubuntu BCC remains pinned to v0.29.0, fetched with a shallow tag clone, and built diff --git a/vhdbuilder/packer/test/linux-vhd-content-test.sh b/vhdbuilder/packer/test/linux-vhd-content-test.sh index 998cd52eea2..6699bb45858 100644 --- a/vhdbuilder/packer/test/linux-vhd-content-test.sh +++ b/vhdbuilder/packer/test/linux-vhd-content-test.sh @@ -73,6 +73,26 @@ assertPackageVersion() { return 0 } +testEmudhraRootRemoval() { + local bundle subjects checked=0 + for bundle in "$@"; do + [ -e "$bundle" ] || [ -L "$bundle" ] || continue + subjects=$(set -o pipefail; openssl crl2pkcs7 -nocrl -certfile "$bundle" | openssl pkcs7 -print_certs -noout) || + { err testEmudhraRootRemoval "Cannot read $bundle"; return 1; } + if ! grep -Eq '^subject=.*CN ?= ?DigiCert Global Root G2(,|$)' <<< "$subjects" || + grep -Eq '^subject=.*CN ?= ?(emSign ECC Root CA - G3|AAA Certificate Services)(,|$)' <<< "$subjects"; then + err testEmudhraRootRemoval "$bundle: control root missing, or emSign/AAA root still trusted" + return 1 + fi + checked=1 + done + [ "$checked" -eq 1 ] || { err testEmudhraRootRemoval "No CA bundle found"; return 1; } +} + +# Check the captured image before test setup can upgrade its CA packages. +testEmudhraRootRemoval /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt \ + /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem || exit 1 + SKIP_GIT_CLONE=false # Git is not present in the base image, so we need to install or bypass it. if [ "$OS_SKU" = "Ubuntu" ]; then diff --git a/vhdbuilder/packer/test/windows-vhd-content-test.ps1 b/vhdbuilder/packer/test/windows-vhd-content-test.ps1 index b6ba5819436..46261d238c0 100644 --- a/vhdbuilder/packer/test/windows-vhd-content-test.ps1 +++ b/vhdbuilder/packer/test/windows-vhd-content-test.ps1 @@ -33,6 +33,20 @@ function Write-OutputWithTimestamp($Message) { Write-Output $msg } +function Test-EmudhraRootRemoval { + $control = 'DF3C24F9BFD666761B268073FE06D1CC8D4F82A4' # DigiCert Global Root G2 + $badRoots = '3043FA4FF257DCA0C380EE2E58EA78B23FE6BBC1|D1EB23A46D17D68FD92564C2F1F1601764D8E349' # emSign ECC G3 / AAA + $roots = @(Get-ChildItem Cert:\LocalMachine\Root, Cert:\LocalMachine\AuthRoot -ErrorAction Stop).Thumbprint + # Windows can install roots from the cached AuthRoot list on demand. + [byte[]]$cache = @((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\SystemCertificates\AuthRoot\AutoUpdate' -Name EncodedCtl -ErrorAction Stop).EncodedCtl) + $cachedRoots = [BitConverter]::ToString($cache).Replace('-', '') + if ($cachedRoots -notmatch $control -or (($roots -join ',') + $cachedRoots) -match $badRoots) { + Write-ErrorWithTimestamp 'Cached control root missing, or emSign/AAA root still trusted or installable' + return $false + } + return $true +} + # $SkipSignatureCheckForBinaries is defined in windows-vhd-configuration.ps1 (dot-sourced above) # so it is shared with windows-files-check.ps1's Test-ValidateSinglePackageSignature. @@ -840,6 +854,8 @@ function Test-ValidateImageBinarySignature { Write-OutputWithTimestamp "Starting Tests" +if (-not (Test-EmudhraRootRemoval)) { exit 1 } + Write-OutputWithTimestamp "Test: FilesToCacheOnVHD" Test-FilesToCacheOnVHD diff --git a/vhdbuilder/packer/test/windows-vhd-content-test.tests.ps1 b/vhdbuilder/packer/test/windows-vhd-content-test.tests.ps1 index c1f745c813f..58e219c9b02 100644 --- a/vhdbuilder/packer/test/windows-vhd-content-test.tests.ps1 +++ b/vhdbuilder/packer/test/windows-vhd-content-test.tests.ps1 @@ -14,6 +14,38 @@ BeforeAll { Invoke-Expression $content } +Describe 'Test-EmudhraRootRemoval' { + BeforeAll { + $script:thumbprints = @{ + control = 'DF3C24F9BFD666761B268073FE06D1CC8D4F82A4' + emSign = '3043FA4FF257DCA0C380EE2E58EA78B23FE6BBC1' + aaa = 'D1EB23A46D17D68FD92564C2F1F1601764D8E349' + } + } + BeforeEach { + Mock Write-ErrorWithTimestamp {} + Mock Get-ChildItem { [pscustomobject]@{ Thumbprint = $script:thumbprints[$Root] } } + Mock Get-ItemProperty { + $hex = if ($Cached -eq 'empty') { '' } else { $script:thumbprints.control + $script:thumbprints[$Cached] } + @{ EncodedCtl = [byte[]]@([regex]::Matches($hex, '..') | ForEach-Object { [Convert]::ToByte($_.Value, 16) }) } + } + } + It 'checks installed and cached roots' -ForEach @( + @{ Root = ''; Cached = ''; Expected = $true } + @{ Root = 'emSign'; Cached = ''; Expected = $false } + @{ Root = 'aaa'; Cached = ''; Expected = $false } + @{ Root = ''; Cached = 'emSign'; Expected = $false } + @{ Root = ''; Cached = 'aaa'; Expected = $false } + @{ Root = ''; Cached = 'empty'; Expected = $false } + ) { + Test-EmudhraRootRemoval | Should -Be $Expected + } + It 'fails when <_> cannot read the store' -ForEach @('Get-ChildItem', 'Get-ItemProperty') { + Mock $_ { throw 'store unreadable' } + { Test-EmudhraRootRemoval } | Should -Throw + } +} + Describe 'Test-PrivatePackageSignature' { BeforeEach { Mock Write-ErrorWithTimestamp {}