From f2d048c8948764c0b8dbec917777f6e6bd7385df Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Mon, 27 Jul 2026 23:00:02 -0400 Subject: [PATCH] Sign on Linux instead of Windows The signing job existed on windows-latest for one reason: `dotnet sign` supports Trusted Signing but cannot run anywhere else, because its backend ships a native signtool Dlib with MFC/MSVC dependencies. `dotnet nuget sign` runs anywhere but only with a local private key, which a key in Trusted Signing does not have. sign-universal closes that gap - it is NuGet's own signing pipeline with the private-key operation redirected to Trusted Signing over HTTPS - so the job moves to ubuntu-latest and drops the node install and the act path shim along with Windows. Nothing else in the pipeline changes. The job keeps its name, its inputs and its outputs, publish still consumes packages-signed, and the signatures produced are ordinary NuGet author signatures: a package signed either way is indistinguishable to consumers. Two notes on the new step. --trust-signing-root is required on Linux and its absence is not obvious: Trusted Signing issues from a root Linux trust stores do not carry, and NuGet refuses to sign against a chain it cannot build, so without the flag the step fails with only "Certificate chain validation failed". The tool version is pinned, because the thing signing a release should not change unless someone chooses to change it. Verified before proposing: the exact command in this job, run against Zomp.SyncMethodGenerator 2.0.38 on Linux with the real credentials, signs and timestamps it and `dotnet nuget verify --all` gives it a clean pass. --- .github/workflows/build.yml | 55 ++++++++++++++----------------------- 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03f6b94..ee14352 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -128,70 +128,57 @@ jobs: # Skip for outside contributors: fork PRs have no access to signing/publish # secrets, so the release pipeline can't do anything useful for them. if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - runs-on: windows-latest + runs-on: ubuntu-latest defaults: run: shell: bash steps: - - name: Set path for nektos/act - if: ${{ runner.os == 'Windows' && env.ACT }} - run: echo "C:\Program Files\Git\bin" >> $GITHUB_PATH - shell: '"C:\Program Files\Git\bin\bash.exe" -c {0}' - - - name: "Determine prerequisites" - id: prerequisite - run: | - echo "need_node=$(command -v node >/dev/null 2>&1 && echo 0 || echo 1)" >> $GITHUB_OUTPUT - - name: Setup .NET - if: ${{ runner.os != 'Windows' || !env.ACT }} uses: actions/setup-dotnet@v5 with: dotnet-version: | 10.0.x - - name: Install node - if: ${{ steps.prerequisite.outputs.need_node == '1' }} - run: | - if [ "${{ runner.os }}" = "windows" ] - then - choco install nodejs -y - echo "C:\Program Files\nodejs" >> $GITHUB_PATH - else - curl -fsSL https://deb.nodesource.com/setup_20.x | bash - &&\ - apt-get install -y nodejs - fi - - name: Download Package artifact uses: actions/download-artifact@v5 with: name: packages path: packages + # Pinned deliberately: the tool that signs the release should not change without + # someone choosing to change it. + - name: Install the signing tool + run: dotnet tool install --global SignUniversal --version 1.0.26-alpha + - name: Sign env: AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNER_CLIENT_SECRET }} AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNER_CLIENT_ID }} if: ${{ env.AZURE_CLIENT_SECRET != '' && github.ref == 'refs/heads/master' }} + # --trust-signing-root is required on Linux and its absence is not obvious. Trusted + # Signing issues from Microsoft Identity Verification Root CA 2020, which Linux trust + # stores do not carry, and NuGet refuses to sign against a chain it cannot build: + # without the flag this step fails with only "Certificate chain validation failed". + # The root is installed for this user alone, from the chain the signing service + # itself returned. run: | - dotnet dnx --prerelease --yes sign code trusted-signing \ - --base-directory "${{ github.workspace }}/packages" \ - "*.nupkg" \ - --trusted-signing-endpoint "${{ secrets.TRUSTED_SIGNING_ENDPOINT }}" \ - --trusted-signing-account "${{ secrets.TRUSTED_SIGNING_ACCOUNT }}" \ - --trusted-signing-certificate-profile "${{ secrets.TRUSTED_SIGNING_CERTIFICATE_PROFILE }}" \ - -v normal + sign-universal sign packages/*.nupkg --trust-signing-root \ + --trusted-signing-endpoint "${{ secrets.TRUSTED_SIGNING_ENDPOINT }}" \ + --trusted-signing-account "${{ secrets.TRUSTED_SIGNING_ACCOUNT }}" \ + --trusted-signing-certificate-profile "${{ secrets.TRUSTED_SIGNING_CERTIFICATE_PROFILE }}" # Catches a signing step which quietly produced nothing usable, while the package can - # still be thrown away. Runs here rather than after publishing because Windows is where - # signature verification needs no extra setup. + # still be thrown away. dotnet nuget verify is cross-platform, so this needs nothing + # Windows-specific: a real Trusted Signing certificate chains to a root in NuGet's own + # bundle, so anything short of a clean pass means nobody downstream could validate the + # package either. - name: Verify signature env: AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNER_CLIENT_SECRET }} if: ${{ env.AZURE_CLIENT_SECRET != '' && github.ref == 'refs/heads/master' }} - run: dotnet nuget verify "${{ github.workspace }}/packages/"*.nupkg --all + run: dotnet nuget verify packages/*.nupkg --all - name: Upload artifacts (.nupkg) uses: actions/upload-artifact@v5