Skip to content

feat: added support for NuGet Packages - #423

Open
igalfsg wants to merge 2 commits into
vcsjones:mainfrom
igalfsg:nuget-package-signing
Open

igalfsg wants to merge 2 commits into
vcsjones:mainfrom
igalfsg:nuget-package-signing

Conversation

@igalfsg

@igalfsg igalfsg commented Oct 18, 2026

Copy link
Copy Markdown

## Summary

Adds support for signing NuGet packages (.nupkg) and symbol packages (.snupkg) with a NuGet **author signature**. This is the same kind of signature dotnet nuget sign produces, but it uses the Key Vault key, so the private key never leaves the vault.

Packages are detected by file extension. They can be signed in the same run as Authenticode files and use the existing options:


azuresigntool sign -kvu https://<vault>.vault.azure.net -kvm -kvc <certificate> \\

&#x20; -fd sha256 -tr http://timestamp.digicert.com -td sha256 \\

&#x20; \*.nupkg \*.dll

## How it works

- NuGetKeyVaultSigner (new, in AzureSign.Core) builds the signature content defined by the NuGet signed package spec. It signs it as CMS through the Key Vault RSA with the signed attributes NuGet requires: signing time, commitment type (proof of origin) and signing-certificate-v2. It can add an RFC 3161 timestamp.

- NuGetPackageArchive (new, internal) reads and rewrites the zip structure the way the spec requires. .signature.p7s is added as a stored entry after the last local file entry, and its central directory record goes after the last existing one. Any existing signature is removed first, and the other entries are copied byte for byte.

- The package is signed in place, just like Authenticode files. The signed result is written back into the original file, so links, ACLs and other file metadata stay.

- Fully managed. It doesn't need mssign32/SIP, and it only builds for net8.0 and later (#if NET).

- Program sends each file to the matching signer and only creates the signers the input files need. --skip-signed works for packages.

## NuGet-specific rules

These are checked up front, with clear error messages:

- --file-digest and --timestamp-digest must be SHA256/384/512. SHA1 is rejected.

- Only RFC 3161 timestamps (-tr). -t (Authenticode timestamp) is rejected.

- --append-signature is rejected, because a package can only have one author signature. An existing signature is replaced unless --skip-signed is used.

- --description, --description-url and the page hashing options have no effect on packages.

- ZIP64 packages are rejected.

The README has a new "NuGet Packages" section covering these.

## Other changes

- Certificate chain building moved into a shared internal CertificateChainBuilder, used by both signers. AlgorithmTranslator gained HashAlgorithmToOid so there is one hash-OID table.

- Timestamp requests for packages use a 30s timeout and up to 3 attempts on transient failures (timeout, 429, 5xx, connection errors). Ctrl+C cancels them.

## Dependencies

- Added System.Security.Cryptography.Pkcs to AzureSign.Core for net8.0 (MIT).

- Added NuGet.Packaging to the test project only (Apache-2.0), so tests can verify signatures with NuGet's own verifier.

- Updated packages to their latest versions. System.Security.Cryptography.Pkcs 10.0.6 has known advisories (GHSA-846f-f764-76mx, GHSA-2fmv-rv2c-59x7). xunit.v3 4.x runs on Microsoft Testing Platform, so a global.json opts dotnet test into it. I'm happy to move the non-Pkcs updates into a separate PR if you prefer.

## Testing

**Unit tests:** all 78 pass (dotnet test). There are 29 new NuGet tests covering:

- Signing with every RSA test certificate and SHA256/384/512, re-signing an already-signed package, .snupkg, and timestamping (DigiCert, like the existing Authenticode tests).

- Verification with NuGet's own PackageSignatureVerifier (integrity plus signature trust and validity, with untrusted roots allowed for the test certificates). The tests also check that every original entry is unchanged.

- Edge cases: archive comments are kept, a .signature.p7s entry with the UTF-8 flag set is detected, a locked package isn't reported as signed, and a read-only package fails without being modified.

- Rejecting SHA1, Authenticode timestamps and ECDSA keys.

**Manual end-to-end test:** a real package signed with an EV code signing certificate in Azure Key Vault, then checked with dotnet nuget verify. Identifying details are redacted:


> azuresigntool sign -kvu https://<vault>.vault.azure.net/ -kvm -kvc <certificate> -fd sha256 -tr http://timestamp.digicert.com -td sha256 -v <package>.nupkg



trce: Retrieving current version of certificate <certificate>.

trce: Retrieved certificate with Id https://<vault>.vault.azure.net/certificates/<certificate>/<version>.

trce: Creating context

info: => File: <package>.nupkg

&#x20;     Signing file.

trce: Hashing package.

trce: Creating package signature.

trce: Timestamping package signature.

info: Signing completed successfully.

info: Successful operations: 1

info: Failed operations: 0


> dotnet nuget verify --all -v detailed <package>.nupkg



Signature Hash Algorithm: SHA256

Signature type: Author

Verifying the author primary signature with certificate:

&#x20; Subject Name: <redacted EV code signing certificate>

&#x20; Issued by: <public EV code signing CA>

&#x20; (chain builds to a trusted code signing root)

Timestamp: 10/18/2026 6:00:23 PM

Verifying author primary signature's timestamp with timestamping service certificate:

&#x20; Subject Name: CN=DigiCert SHA256 RSA4096 Timestamp Responder 2026 1, O="DigiCert, Inc.", C=US

&#x20; Issued by: CN=DigiCert Trusted G4 TimeStamping RSA4096 SHA256 2025 CA1, O="DigiCert, Inc.", C=US

&#x20; (chain builds to DigiCert Trusted Root G4)



Successfully verified package '<package>'.

## Notes

- NuGetKeyVaultSigner is a new public type in AzureSign.Core. AuthenticodeKeyVaultSigner's public API is unchanged.

- The signer is fully managed, but azuresigntool still only runs on Windows. I didn't change that in this PR.

## Disclosure
I used AI to help with the code but I manually tested everything

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant