Skip to content

Add signing implementation to ImageBuilder#1957

Merged
lbussell merged 55 commits intodotnet:mainfrom
lbussell:signing-src
Feb 20, 2026
Merged

Add signing implementation to ImageBuilder#1957
lbussell merged 55 commits intodotnet:mainfrom
lbussell:signing-src

Conversation

@lbussell
Copy link
Member

@lbussell lbussell commented Jan 30, 2026

Related:

What's included in this PR:

Summary of types added

Signing

  • Data types:
    • ImageSigningRequest
    • PayloadSigningResult
    • ImageSigningResult
  • Services:
    • BulkImageSigningService : IBulkImageSigningService - orchestrates signing and pushing signatures
    • EsrpSigningService : IEsrpSigningService - invokes DDSignFiles.dll via MicroBuild
    • PayloadSigningService : IPayloadSigningService - writes payloads, signs via ESRP, calculates cert chains
    • SigningRequestGenerator : ISigningRequestGenerator - creates requests from ImageArtifactDetails
  • CertificateChainCalculator - static class to extract x5chain from COSE envelopes
  • SigningServiceExtensions - DI registration extension methods

Oras

  • OrasCredentialProviderAdapter - adapts ImageBuilder credentials to ORAS auth
  • OrasDotNetService : IOrasDescriptorService, IOrasSignatureService - ORAS .NET library implementation

Configuration

  • BuildConfiguration - for build/pipeline artifact settings
  • SigningConfiguration - for ESRP signing settings

Tests

  • OrasCredentialProviderAdapterTests
  • SigningRequestGeneratorTests

What's not included in this PR:

  • Integration into Build or Post-Build commands or pipeline steps It's a separate command, now follows original design outlined in the issue.
  • Comprehensive tests Added.
  • Pipeline yaml - follow-up PR
  • Signing validation - follow-up PR

TODO (before leaving draft mode):

  • Successful internal test run end-to-end in pipeline
  • Add more tests
  • Cleanup

Introduce configuration and model types for container image signing:

- SigningConfiguration: holds ESRP certificate IDs for images and referrers
- BuildConfiguration: holds ArtifactStagingDirectory for build artifacts
- Add Signing property to PublishConfiguration
- ImageSigningRequest, PayloadSigningResult, ImageSigningResult records

This is Phase 1 of the signing implementation.
Introduce ORAS services using OrasProject.Oras 0.4.0 for pushing
Notary v2 signatures to registries:

- IOrasDescriptorService: resolves OCI descriptors from references
- IOrasSignatureService: pushes signatures as referrer artifacts
- OrasCredentialProviderAdapter: bridges IRegistryCredentialsProvider
- Uses Packer.PackManifestAsync with Subject for referrer relationship

Existing IOrasClient remains unchanged for other functionality.
Test credential mapping, null handling, and host passthrough.
Implement the core signing service layer:

- IEsrpSigningService: invokes DDSignFiles.dll via MicroBuild plugin
- IPayloadSigningService: writes payloads, signs via ESRP, calculates cert chain
- IBulkImageSigningService: orchestrates signing and ORAS push
- CertificateChainCalculator: extracts x5chain thumbprints from COSE envelopes

Also adds GetEnvironmentVariable to IEnvironmentService and
SigningServiceExtensions for DI registration.
Implements ISigningRequestGenerator with two methods:
- GeneratePlatformSigningRequestsAsync: Converts platform digests to signing requests
- GenerateManifestListSigningRequestsAsync: Converts manifest list digests to signing requests

Uses LINQ to flatten the repo/image/platform hierarchy. Updates BuildCommand
to use the generator instead of inline request creation.
This standalone command has been superseded by the integrated signing
services that sign images immediately after build/push in BuildCommand.
- Add SignType property to SigningConfiguration (defaults to 'test')
- Update EsrpSigningService to use SignType from config
- Set SignType: real in publish-config-prod.yml
- Set SignType: test in publish-config-nonprod.yml
- Add Enabled property to SigningConfiguration
- Update BuildCommand to check Signing.Enabled before signing
- Update publish-config templates to use $(enableSigning) variable
- Update init-imagebuilder to read enableSigning and signType from variables

To enable signing in a pipeline, set:
  variables:
    enableSigning: true
    signType: real  # or 'test'
Inject IBulkImageSigningService and call SignImagesAsync after push.
Signing is skipped in dry-run mode or when SigningConfiguration is null.
Some image-info entries have null digests when their platforms
weren't built in the current pipeline run. ApplyOverrideToDigest
assumes non-null input, causing a NullReferenceException in the
signing command. The signing request generator already filters
out empty digests downstream, but the override runs first.
Extract signing logic from BuildCommand into a new standalone SignImagesCommand.
This creates SignImagesCommand.cs and SignImagesOptions.cs for separate signing.
Removes signing-related fields and methods from BuildCommand.
Registers SignImagesCommand in ImageBuilder.
Add RegistryOverride option to SignImagesOptions and apply it
to image artifact details before generating signing requests.
DDSignFiles.dll requires VSENGESRPSSL environment variable on non-Windows
platforms. Add fail-fast check in EsrpSigningService to catch missing
environment variables before DDSignFiles retries auth endlessly.
After ApplyRegistryOverride, the digest is already a fully qualified
reference (registry/prefix/repo@sha256:...). The signing request
generator was incorrectly prepending repo@ again, producing a
malformed reference.
Switch from runtime-deps to runtime base image and remove
--self-contained=true from dotnet publish. This provides the
dotnet CLI in the container, which is needed to invoke
DDSignFiles.dll for ESRP signing.
DDSignFiles expects SignFileRecordList with Certs/SrcPath/DstPath
fields. We were generating the wrong schema (MicroBuild SignBatches
format), causing a NullReferenceException in GetSignRecordsFromInputFile.
Update test mocks and assertions to match the current behavior where
SigningRequestGenerator passes bare digests (e.g. 'sha256:abc123')
instead of full image references to the descriptor service.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements container image signing functionality for ImageBuilder using ESRP (Enterprise Signing and Release Pipeline) and ORAS (OCI Registry as Storage). The implementation replaces the previous GenerateSigningPayloadsCommand with a new end-to-end SignImagesCommand that handles payload generation, ESRP signing, certificate chain extraction, and signature artifact publishing.

Changes:

  • Added comprehensive signing infrastructure with services for ESRP integration, payload signing, and ORAS-based signature publishing
  • Integrated ORAS .NET library for OCI descriptor resolution and signature artifact management
  • Added configuration models for signing and build artifacts
  • Removed old GenerateSigningPayloadsCommand in favor of new SignImagesCommand
  • Modified Dockerfile to use framework-dependent deployment instead of self-contained

Reviewed changes

Copilot reviewed 38 out of 38 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
SigningServiceExtensions.cs DI registration extension methods for signing services
SigningRequestGenerator.cs Generates signing requests by fetching OCI descriptors from registries
PayloadSigningService.cs Writes payloads to disk, invokes ESRP signing, and calculates certificate chains
EsrpSigningService.cs Wraps MicroBuild DDSignFiles.dll for ESRP file signing
CertificateChainCalculator.cs Extracts and calculates x5chain thumbprints from COSE signature envelopes
BulkImageSigningService.cs Orchestrates bulk signing and signature publishing workflow
OrasDotNetService.cs ORAS .NET library integration for descriptor resolution and signature pushing
OrasCredentialProviderAdapter.cs Adapts ImageBuilder credentials to ORAS authentication interface
IOrasSignatureService.cs, IOrasDescriptorService.cs Interfaces for ORAS operations
SignImagesCommand.cs Command implementation that orchestrates end-to-end signing process
SignImagesOptions.cs CLI options for sign-images command
SigningConfiguration.cs, BuildConfiguration.cs Configuration models for signing and build artifacts
PublishConfiguration.cs Added Signing property
ConfigurationExtensions.cs Added BuildConfiguration DI registration
ImageBuilder.cs Updated DI registrations, removed old command, added new command and ORAS services
IEnvironmentService.cs, EnvironmentService.cs Added GetEnvironmentVariable method
ImageInfoHelper.cs Added null/empty checks before applying registry overrides
BuildCommand.cs Removed unused PublishConfiguration parameter
Microsoft.DotNet.ImageBuilder.csproj Added OrasProject.Oras and System.Formats.Cbor packages
Dockerfile.linux Changed to framework-dependent deployment and runtime base image
SigningRequestGeneratorTests.cs Tests for request generator
CertificateChainCalculatorTests.cs Tests for certificate chain calculation
OrasDotNetServiceTests.cs Tests for ORAS service (partial coverage)
OrasCredentialProviderAdapterTests.cs Tests for credential adapter
GenerateSigningPayloadsCommandTests.cs Removed old command tests
BuildCommandTests.cs Updated test helper to remove unused parameter
ImageSigningRequest.cs, ImageSigningResult.cs, PayloadSigningResult.cs Data transfer objects for signing operations
ISigningRequestGenerator.cs, IPayloadSigningService.cs, IEsrpSigningService.cs, IBulkImageSigningService.cs Service interfaces

lbussell and others added 4 commits February 18, 2026 11:44
Add ArgumentException/ArgumentNullException guards to GetDescriptorAsync
and PushSignatureAsync. Update test to expect ArgumentNullException
instead of NullReferenceException.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add ArgumentException guard for signedPayloadPath parameter. Handle
nullable int? from ReadStartArray/ReadStartMap with explicit exceptions
for indefinite-length CBOR structures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add TODO for unused ReferrerSigningKeyCode property. Clarify Task.Run
cancellation token behavior in EsrpSigningService.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
lbussell and others added 7 commits February 20, 2026 09:49
…ImageSigningService class

Fix constructor argument order in ImageSigningServiceTests

The mock arguments for IOrasSignatureService and IPayloadSigningService
were swapped, not matching the ImageSigningService constructor order.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move the dry-run check after image-info.json parsing, registry override
application, and digest extraction. This validates pipeline
configuration
and logs what would be signed, matching the dry-run pattern used by
CopyImageService and other commands.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lbussell
Copy link
Member Author

Ready for a second look.

lbussell and others added 5 commits February 20, 2026 11:46
Collapse the PayloadSigningService abstraction layer into ImageSigningService.
PayloadSigningService was only consumed by ImageSigningService and served as
an unnecessary intermediary between it and EsrpSigningService.

This removes IPayloadSigningService, PayloadSigningService, and the
WrittenPayload record. The payload-writing, ESRP invocation, and cert chain
calculation logic now lives directly in ImageSigningService as a private
SignPayloadsAsync method.

Tests are updated to mock IEsrpSigningService directly instead of
IPayloadSigningService, and the missing-artifact-dir test is ported from
PayloadSigningServiceTests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the parameterless overload that instantiated FileSystem directly,
bypassing DI. All production callers pass IFileSystem explicitly.

Update CertificateChainCalculatorTests to use InMemoryFileSystem instead of
writing to the real filesystem, eliminating disk I/O and the IDisposable
cleanup pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lbussell
Copy link
Member Author

I am going to combine the two new Oras interfaces as well as combine the PayloadSigningService into ImageSigningService.

lbussell and others added 2 commits February 20, 2026 13:17
Replace the private ExtractAllImageDigests method with the pre-existing
ImageInfoHelper.GetAllDigests extension method, filtering out null/empty
digests at the call site. This eliminates duplicated digest extraction logic
between ImageSigningService and SignImagesCommand.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lbussell lbussell enabled auto-merge (squash) February 20, 2026 22:26
@lbussell lbussell merged commit 22bd4c6 into dotnet:main Feb 20, 2026
12 checks passed
@lbussell lbussell deleted the signing-src branch February 20, 2026 22:45
lbussell added a commit that referenced this pull request Feb 27, 2026
… `ManifestHelper` (#1986)

Part of #1973. Extract
manifest JSON loading out of ManifestInfo static methods and into a
dedicated `ManifestJsonService`, making the I/O dependency
explicit/injectable/mockable. `ManifestJsonService` also uses the new
`IFileSystem` abstraction introduced in #1957.
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.

4 participants