Skip to content

Support NuGet package signing using Azure Artifact Signing on Linux#10

Closed
Bradley Grainger (bgrainger) wants to merge 4 commits into
Devolutions:masterfrom
bgrainger:fix-linux-azure-nuget-signing
Closed

Support NuGet package signing using Azure Artifact Signing on Linux#10
Bradley Grainger (bgrainger) wants to merge 4 commits into
Devolutions:masterfrom
bgrainger:fix-linux-azure-nuget-signing

Conversation

@bgrainger
Copy link
Copy Markdown
Contributor

@bgrainger Bradley Grainger (bgrainger) commented May 22, 2026

Disclaimer

I understand none of this code; it was produced entirely by GitHub Copilot using GPT-5.5.

The point of this PR is to demonstrate that with these changes, psign-tool could produce a correctly-signed NuGet package using Azure Artifact Signing under Linux (Ubuntu 24.04 on WSL2). I'm opening this PR in case having this information is helpful for producing a new version of psign that has this support built-in; I'm not expecting this to be merged as-is (although it's fine if you do).

I am interested in getting this scenario working, and am willing to produce more targeted fixes (with AI) if required. (Note that I am a C# Windows developer not a Linux Rust developer.)

AI Generated Text Below

Summary

This makes the portable/Linux signing path work with Azure Artifact Signing responses and produce NuGet packages that dotnet nuget verify accepts. It keeps the existing Authenticode timestamp behavior, while using the CMS timestamp attribute NuGet expects for package signatures.

Before

A live Azure Artifact Signing response could not be consumed reliably on Linux:

  • The signingCertificate field was assumed to be a PEM/DER X.509 certificate. In practice it can be base64 text wrapping a PKCS#7 certificate bag, so both portable sign-pe and code --mode portable failed while parsing the returned certificate payload.
  • Some certificate bags have no SignerInfo, so choosing the first embedded certificate is not safe. The observed bag order could put a root/intermediate certificate before the short-lived leaf signing certificate, producing signatures attributed to the wrong certificate.
  • Once package signing reached the ZIP-writing stage, NuGet rejected the package with NU3005 because the central-directory version made by / external attributes carried Unix file metadata.
  • After clearing ZIP metadata, NuGet rejected the package with NU3000 because the package .signature.p7s used detached CMS content. NuGet package signatures need the id-data content embedded.
  • Timestamping used the Microsoft Authenticode RFC3161 unsigned attribute OID. NuGet did not treat that as a package-signature timestamp; it expects id-aa-timeStampToken (1.2.840.113549.1.9.16.2.14).

Changes

  • Add a shared Artifact Signing certificate parser in psign-sip-digest that accepts PEM, DER, base64-wrapped data, and PKCS#7 certificate bags.
  • Resolve signer certificates from SignerInfo when present, and for certificate-only bags select an unambiguous leaf/code-signing candidate instead of assuming certificate-set order.
  • Reuse the shared parser from both the portable digest CLI and the code command.
  • Normalize NuGet ZIP central-directory host OS and external attributes when removing or embedding .signature.p7s.
  • Let sign_pkcs7_id_data choose detached vs attached CMS content per caller: NuGet uses attached id-data, while App Installer/AuthentiCode-style companion signatures remain detached.
  • Add a CMS/PKCS#9 RFC3161 timestamp helper and use id-aa-timeStampToken for NuGet package signatures, while preserving the Microsoft Authenticode timestamp OID for Authenticode paths.
  • Add regression coverage for PKCS#7 certificate bags, nested/base64-wrapped Artifact Signing certificate payloads, no-SignerInfo certificate bags, NuGet ZIP metadata normalization, and NuGet timestamp OID behavior.

Testing

Automated:

cargo test -p psign-digest-cli --features artifact-signing-rest --locked --quiet
cargo test -p psign --test code_command --locked --quiet
cargo test -p psign-sip-digest --locked --quiet
cargo test -p psign-opc-sign --locked --quiet

Manual/live, with private Azure Artifact Signing configuration redacted and no service URLs included:

  • Built psign-tool on Linux from this branch.
  • Signed a PE/DLL payload using an Azure Artifact Signing account/profile; the signature used the expected short-lived organization code-signing leaf certificate rather than a Microsoft root/intermediate certificate.
  • Signed a NuGet package containing an inner DLL using psign-tool code --mode portable with Azure Artifact Signing and RFC3161 timestamping.
  • Verified the signed package with dotnet nuget verify -v detailed; verification succeeded and NuGet recognized the timestamp.
  • Extracted and verified the inner DLL signature with psign-tool portable verify-pe.

Follow-up: NuGet publisher metadata

A later commit adds the NuGet author signed attributes needed for package tooling to classify the primary package signature as an author/publisher signature: signingTime, id-smime-aa-ets-commitmentType with proofOfOrigin, and signingCertificateV2/ESSCertIDv2. This is what allows NuGetPackageExplorer to show the Publisher UI instead of only reporting Signature: Valid.

Additional validation after that commit:

cargo clippy -p psign-sip-digest -p psign-digest-cli -p psign-authenticode-trust -p psign-portable-core -p psign-portable-ffi -p psign-codesigning-rest -p psign-azure-kv-rest --all-targets --locked -- -D warnings
dotnet nuget verify --all <redacted-linux-signed-package>
# Output included: Signature type: Author

Enable portable Artifact Signing responses to be consumed on Linux by accepting PEM/DER/base64/PKCS#7 signingCertificate payloads and selecting the actual signing cert from PKCS#7 signer info or an unambiguous leaf certificate bag.

Make NuGet package signing produce verifiable packages by normalizing ZIP metadata, embedding CMS id-data content, and using the standard id-aa-timeStampToken timestamp attribute while preserving Authenticode timestamp behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bgrainger Bradley Grainger (bgrainger) marked this pull request as draft May 22, 2026 22:18
@bgrainger Bradley Grainger (bgrainger) changed the title Support Linux Azure NuGet package signing Support NuGet package signing using Azure Artifact Signing on Linux May 22, 2026
Add the NuGet author signed-attribute profile needed for publisher metadata: signing-time, commitment-type proof-of-origin, and signing-certificate-v2. Remote signing now obtains the signer certificate before hashing CMS signed attributes so the signing-certificate-v2 ESSCertIDv2 value is part of the signed payload.

This makes Linux-generated NuGet signatures classify as Author signatures in NuGet tooling, which enables NuGetPackageExplorer to display the Publisher UI for packages signed through Azure Artifact Signing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bgrainger
Copy link
Copy Markdown
Contributor Author

Bradley Grainger (bgrainger) commented May 22, 2026

The second commit address this problem: NuGet Package Explorer was not showing "Publisher:" metadata as displayed in this screenshot. Previously, only the "Signature ✅" line was showing; now the other metadata (with a link to a Windows certificate dialog) appears.

Reverse-engineered by pointing GPT-5.5 at the https://github.com/NuGetPackageExplorer/NuGetPackageExplorer source code.

image

AI Text Below

Follow-up: added NuGet author signed attributes required once packages are classified as publisher/author signatures. The final package signature now includes signing-time, id-smime-aa-ets-commitmentType with proofOfOrigin, and signing-certificate-v2/ESSCertIDv2. A redacted live Azure Artifact Signing run produced a package that dotnet nuget verify --all reports as Signature type: Author with the expected publisher subject, which is the metadata path NuGetPackageExplorer uses to show the Publisher UI.

@mamoreau-devolutions
Copy link
Copy Markdown
Contributor

Bradley Grainger (@bgrainger) are the changes on this branch making it work for a successful replacement of your current code signing tools? You could remove the draft flag from this PR. There are some code formatting checks to fix for the CI

@bgrainger
Copy link
Copy Markdown
Contributor Author

a successful replacement of your current code signing tools?

I have not tested it E2E but that's the goal.

The current signing code is here: https://github.com/Faithlife/FaithlifeBuild/blob/20d69035d606ccc4e346512802f51fb3b0152d27/src/Faithlife.Build/DotNetBuild.cs#L551-L590

My intention is to either detect Linux and install psign, or use psign unconditionally on all platforms in that code, then migrate the signing arguments to be compatible with psign. To that end, it would be more convenient to use command-line flags (to psign) for Azure Artifact Signing instead of having to write out a temporary JSON file with some of the arguments. (Edit: reading the code it looks like --artifact-signing-endpoint etc. is supported; my LLM test harness just latched onto --artifact-signing-metadata first.)

One point of friction is that signtool integrates directly with Azure.Identity (see https://github.com/dotnet/sign/blob/0ddd0b9a9be5d5be986e94024642f36d0e2c1a31/docs/artifact-signing-integration.md?plain=1#L7) and psign would need me to do az account get-access-token then pass in --artifact-signing-access-token. I don't think that's a big deal and can probably just use azure/login@v3 and azure/cli@v2 in GHA for those pieces?

Plan:

  1. Wait for updated version of Devolutions.Psign.Tool that fixes the issues identified in this PR
  2. Update Faithlife.Build to install and use it on Linux
  3. Publish pre-release version of Faithlife.Build; switch one private GHA build to ubuntu-latest
  4. Test and report back

@bgrainger Bradley Grainger (bgrainger) marked this pull request as ready for review May 23, 2026 17:55
The NuGet author-signature path needs a helper that assembles CMS SignedData from explicit content, digest, signer material, signature bytes, detached-content behavior, and prebuilt signed attributes. Allow Clippy's argument-count lint for this narrowly scoped helper so the portable-clippy CI job passes without changing behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mamoreau-devolutions
Copy link
Copy Markdown
Contributor

Bradley Grainger (@bgrainger) a way to do the full testing would be to temporarily modify your real workflow to pull your branch, build psign-tool from source and attempt using it to code sign your nuget without actually publishing it. this is how I got Azure Key Vault signing confirmed to be working by code signing psign-tool with it - there were a couple of things to fix that hadn't been caught until I tried code signing for real.

@bgrainger
Copy link
Copy Markdown
Contributor Author

attempt using it to code sign your nuget without actually publishing it

This was basically what I did. That's how I got the screenshot from NuGet Package Explorer; I iterated on it until it (appeared to) produced the exact same output as a build on Windows.

I'm pretty confident that with this PR's changes I can modify Faithlife.Build to sign with psign and get the correct output; I know I need to modify it to run:

TOKEN=$(az account get-access-token \
  --resource https://codesigning.azure.net/ \
  --query accessToken -o tsv)
"$PSIGN" code --mode portable --verbose \
  --artifact-signing-metadata release/sign/artifact-signing-metadata.json \
  --artifact-signing-access-token "$TOKEN" \
  --timestamp-url http://timestamp.acs.microsoft.com/ \
  --timestamp-digest sha256 \
  --output signed/SomePackage.9.35.0.nupkg \
  release/SomePackage.9.35.0.nupkg

The only thing that hasn't been tested is running it on ubuntu-latest instead of WSL2 (then uploading to GitHub Actions artifacts for manual verification). I can work on that next week (note that 25 May is a U.S. holiday).

Allow the shared PKCS#7 id-data helper to exceed clippy's argument-count threshold, matching the earlier targeted suppression on the related CMS helper and unblocking the portable clippy job without changing behavior.

Also extend the generated test certificate lifetime in cli_pe_digest so the timestamped verification cases still pass when they intentionally validate at tomorrow-noon UTC. This keeps the CI workflow stable while preserving the same end-to-end coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Marc-André Moreau (mamoreau-devolutions) added a commit that referenced this pull request May 24, 2026
## Summary

This imports the useful work from #10 and cleans it up for a reviewable
implementation of portable/Linux NuGet package signing with Azure
Artifact Signing.

- parses Artifact Signing `signingCertificate` payloads as DER, PEM,
nested base64, or PKCS#7 certificate bundles, selecting the actual
signer instead of relying on certificate order
- produces NuGet-compatible `.signature.p7s` CMS with attached
`id-data`, NuGet author attributes, and PKCS#9 RFC3161 timestamp
attributes while preserving Authenticode timestamp behavior for
companion signatures
- normalizes NuGet ZIP central-directory metadata so Unix-created
packages do not carry host/external attributes that NuGet rejects
- expands deterministic tests for certificate parsing, ZIP
normalization, local/package signing, timestamp OID behavior, and
Artifact Signing/server-backed flows

## Validation

- `cargo fmt --all --check`
- `cargo clippy --workspace --all-targets --locked -- -D warnings`
- `cargo test --workspace --locked --quiet`

---------

Co-authored-by: Bradley Grainger <bradley.grainger@logos.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mamoreau-devolutions
Copy link
Copy Markdown
Contributor

Excellent 👍 I have cleaned up and fixed your changes to pass the CI checks in #11 I'll make a new release in the coming days

@mamoreau-devolutions
Copy link
Copy Markdown
Contributor

Bradley Grainger (@bgrainger) I just released 0.4.0, let me know if it works for you when you have the chance

@bgrainger
Copy link
Copy Markdown
Contributor Author

Marc-André Moreau (@mamoreau-devolutions) We are so close! I got psign working with this command-line:

[
	"code",
	"--mode", "portable",
	"--verbose",
	"--artifact-signing-endpoint", "https://wus2.codesigning.azure.net",
	"--artifact-signing-account-name", "...",
	"--artifact-signing-profile-name", certificateProfile,
	"--artifact-signing-access-token", token,
	"--timestamp-url", "http://timestamp.acs.microsoft.com/",
	"--timestamp-digest", "sha256",
	"--output", signedPackagePath,
	packagePath,
]

It was only after doing so that I noticed that the NuGet package itself is timestamped, but the DLLs embedded in it are signed but not timestamped. This is likely an oversight in my last PR so will be repro-ing this E2E and opening a new PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants