Repository navigation
CI: add eMudhra/emSign CA stopgap for GitHub-hosted Windows runners #3270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Temporary stopgap for the eMudhra/emSign CA key compromise (CVE-2026-111943) | ||
| # on GitHub-hosted runners until GitHub ships fixed images. | ||
| # | ||
| # On Linux: upgrades ca-certificates via apt so the compromised emSign ECC Root | ||
| # CA - G3 anchor is removed from /etc/ssl/certs/ca-certificates.crt. Ubuntu's | ||
| # bundle does not include AAA Certificate Services, so the Sectigo cross-signs | ||
| # do not chain to a trusted root there. | ||
| # On Windows: adds the eight compromised intermediates and their eight Sectigo | ||
| # cross-signs (impacted-intermediates.json) to Cert:\LocalMachine\Disallowed so | ||
| # schannel-based clients (git, gh, PowerShell/.NET) reject chains through them, | ||
| # including chains to AAA Certificate Services, which Windows still trusts. | ||
| # | ||
| # This action is a no-op on self-hosted runners (runner.environment != 'github-hosted') | ||
| # because self-hosted runner operators own and update their own host images. | ||
| # | ||
| # Removal criteria and implementation notes: security/emudhra-plan2.md | ||
| # Tracked by: actions/runner-images#15018 | ||
|
|
||
| name: Harden OS certificate trust (eMudhra/emSign CA stopgap) | ||
| description: > | ||
| Temporary per-job stopgap for CVE-2026-111943. Upgrades ca-certificates on | ||
| GitHub-hosted Linux runners and adds the compromised eMudhra intermediates | ||
| and cross-signs to the Disallowed store on GitHub-hosted Windows runners. | ||
| No-op on self-hosted. | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
|
|
||
| # ------------------------------------------------------------------------- | ||
| # Windows path: add the compromised intermediates to LocalMachine\Disallowed. | ||
| # Takes effect immediately for all schannel-based clients in subsequent steps | ||
| # (git, gh, PowerShell/.NET, WinHTTP). Must precede actions/checkout. | ||
| # ------------------------------------------------------------------------- | ||
| - name: Distrust compromised eMudhra intermediates (Windows, GitHub-hosted) | ||
| if: ${{ runner.os == 'Windows' && runner.environment == 'github-hosted' }} | ||
| shell: pwsh | ||
| env: | ||
| ACTION_PATH: ${{ github.action_path }} | ||
| run: | | ||
| $ErrorActionPreference = 'Stop' | ||
| . (Join-Path $env:ACTION_PATH 'emudhra_intermediates.ps1') | ||
|
|
||
| $certs = @(Get-EmudhraIntermediates) | ||
| try { | ||
| # Adding is idempotent, so no early exit when the image is already fixed. | ||
| $store = [System.Security.Cryptography.X509Certificates.X509Store]::new('Disallowed', 'LocalMachine') | ||
| $store.Open('ReadWrite') | ||
| try { | ||
| foreach ($cert in $certs) { $store.Add($cert) } | ||
| } finally { | ||
| $store.Close() | ||
| } | ||
|
|
||
| $notDistrusted = @($certs | Where-Object { -not (Test-ExplicitlyDistrusted $_) } | ForEach-Object { $_.EmudhraLabel }) | ||
| if ($notDistrusted.Count -gt 0) { | ||
| Write-Error "Not explicitly distrusted after adding to LocalMachine\Disallowed: $($notDistrusted -join '; ')" | ||
| exit 1 | ||
| } | ||
| Write-Host "Distrusted $($certs.Count) compromised eMudhra intermediates and cross-signs." | ||
| } finally { | ||
| $certs | ForEach-Object { $_.Dispose() } | ||
| } | ||
|
|
||
| # ------------------------------------------------------------------------- | ||
| # Linux path: upgrade ca-certificates via apt. | ||
| # The Ubuntu archive GPG check does not depend on the trust store being | ||
| # fixed, so this bootstrap is safe. Retries guard against transient mirror | ||
| # failures. Must precede actions/checkout. | ||
| # ------------------------------------------------------------------------- | ||
| - name: Upgrade ca-certificates (Linux, GitHub-hosted) | ||
| if: ${{ runner.os == 'Linux' && runner.environment == 'github-hosted' }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| EMSIGN_G3_FP="86:A1:EC:BA:08:9C:4A:8D:3B:BE:27:34:C6:12:BA:34:1D:81:3E:04:3C:F9:E8:A8:62:CD:5C:57:A3:6B:BE:6B" | ||
| CA_BUNDLE="/etc/ssl/certs/ca-certificates.crt" | ||
| VERSION_ID="$(. /etc/os-release && echo "${VERSION_ID}")" | ||
|
|
||
| # Fail closed if openssl is missing or the bundle cannot be parsed | ||
| # (mirrors ci/tools/runner_trust_probe.sh). | ||
| if ! command -v openssl >/dev/null 2>&1; then | ||
| echo "::error::openssl not found; cannot verify CA bundle" | ||
| exit 1 | ||
| fi | ||
| if [[ ! -f "${CA_BUNDLE}" ]]; then | ||
| echo "::error::CA bundle not found: ${CA_BUNDLE}" | ||
| exit 1 | ||
| fi | ||
|
|
||
| fingerprints_in_bundle() { | ||
| awk '/-----BEGIN CERTIFICATE-----/{cert=""} {cert=cert $0 "\n"} | ||
| /-----END CERTIFICATE-----/{print cert | "openssl x509 -noout -fingerprint -sha256 2>/dev/null"; close("openssl x509 -noout -fingerprint -sha256 2>/dev/null")}' \ | ||
| "${CA_BUNDLE}" | ||
| } | ||
|
|
||
| fps="$(fingerprints_in_bundle)" | ||
| cert_count="$(grep -c . <<< "${fps}" || true)" | ||
| if (( cert_count == 0 )); then | ||
| echo "::error::No certificates parsed from ${CA_BUNDLE}; openssl may have failed" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if grep -qiF "${EMSIGN_G3_FP}" <<< "${fps}"; then | ||
| before_state="not fixed" | ||
| else | ||
| before_state="already fixed" | ||
| fi | ||
| before_ver="$(dpkg-query -W -f='${Version}' ca-certificates 2>/dev/null || echo unknown)" | ||
| echo "::notice::emSign ECC Root CA - G3 on ${VERSION_ID}: ${before_state} (ca-certificates ${before_ver})" | ||
|
|
||
| if [ "${before_state}" = "already fixed" ]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| for attempt in 1 2 3; do | ||
| if sudo apt-get update -q && sudo apt-get install --only-upgrade -y ca-certificates; then | ||
| break | ||
| fi | ||
| if [ "${attempt}" -lt 3 ]; then | ||
| echo "apt-get attempt ${attempt} failed; retrying in 10s..." | ||
| sleep 10 | ||
| else | ||
| echo "::error::apt-get upgrade failed after ${attempt} attempts" | ||
| exit 1 | ||
| fi | ||
| done | ||
|
|
||
| after_ver="$(dpkg-query -W -f='${Version}' ca-certificates 2>/dev/null || echo unknown)" | ||
| after_fps="$(fingerprints_in_bundle)" | ||
| after_count="$(grep -c . <<< "${after_fps}" || true)" | ||
| if (( after_count == 0 )); then | ||
| echo "::error::No certificates parsed after upgrade; cannot verify fix" | ||
| exit 1 | ||
| fi | ||
| if grep -qiF "${EMSIGN_G3_FP}" <<< "${after_fps}"; then | ||
| echo "::error::emSign ECC Root CA - G3 still present after upgrade (ca-certificates ${after_ver} on ${VERSION_ID})" | ||
| exit 1 | ||
| fi | ||
| echo "ca-certificates upgraded to ${after_ver}; emSign ECC Root CA - G3 absent." | ||
50 changes: 50 additions & 0 deletions
50
.github/actions/upgrade-ca-certificates/emudhra_intermediates.ps1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Helpers for the compromised eMudhra intermediates and their Sectigo | ||
| # cross-signs listed in impacted-intermediates.json. Dot-sourced by this | ||
| # action's Windows step and by ci/tools/runner_trust_probe.ps1. | ||
| # | ||
| # impacted-intermediates.json is the disclosure's file, unmodified (SHA-256 | ||
| # f67b3abe9b4879f4e91874ed041beccd71257f1db41c588c724ccf12aa90c59e). The set | ||
| # is closed: new certificates for these keys would have to be issued by | ||
| # another CA, and all existing ones are revoked. | ||
|
|
||
| $EmudhraIntermediatesPath = Join-Path $PSScriptRoot 'impacted-intermediates.json' | ||
| $EmudhraIntermediatesCount = 16 | ||
|
|
||
| # Returns the certificates as X509Certificate2 objects; callers dispose them. | ||
| function Get-EmudhraIntermediates { | ||
| $entries = @(Get-Content -LiteralPath $EmudhraIntermediatesPath -Raw | ConvertFrom-Json) | ||
| if ($entries.Count -ne $EmudhraIntermediatesCount) { | ||
| throw "Expected $EmudhraIntermediatesCount certificates in $EmudhraIntermediatesPath, found $($entries.Count)." | ||
| } | ||
| foreach ($entry in $entries) { | ||
| $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromPem($entry.pem) | ||
| if ($cert.GetCertHashString('SHA256') -ne $entry.sha256.ToUpperInvariant()) { | ||
| $cert.Dispose() | ||
| throw "SHA-256 mismatch for $($entry.name) (issued by $($entry.issued_by))." | ||
| } | ||
| Add-Member -InputObject $cert -NotePropertyName EmudhraLabel -NotePropertyValue "$($entry.name) (issued by $($entry.issued_by))" | ||
| $cert | ||
| } | ||
| } | ||
|
|
||
| # True if Windows chain building reports the certificate as explicitly | ||
| # distrusted. This covers both a local LocalMachine\Disallowed entry and | ||
| # Microsoft's automatically updated disallowed list. Revocation checking and | ||
| # AIA downloads are off so the result reflects the local trust state only. | ||
| function Test-ExplicitlyDistrusted([System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert) { | ||
| $explicitDistrust = [System.Security.Cryptography.X509Certificates.X509ChainStatusFlags]::ExplicitDistrust | ||
| $chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new() | ||
| try { | ||
| $chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck | ||
| $chain.ChainPolicy.DisableCertificateDownloads = $true | ||
| [void]$chain.Build($Cert) | ||
| $statuses = @($chain.ChainStatus) + @($chain.ChainElements | ForEach-Object { $_.ChainElementStatus }) | ||
| return [bool]($statuses | Where-Object { $_.Status.HasFlag($explicitDistrust) }) | ||
| } finally { | ||
| $chain.Dispose() | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.