Skip to content

Add support for pkcs12 format certificate trust bundles to enable trust in Java service scenarios#14756

Open
danegsta wants to merge 9 commits intorelease/13.2from
danegsta/pkcs12
Open

Add support for pkcs12 format certificate trust bundles to enable trust in Java service scenarios#14756
danegsta wants to merge 9 commits intorelease/13.2from
danegsta/pkcs12

Conversation

@danegsta
Copy link
Member

@danegsta danegsta commented Feb 27, 2026

Description

Adds a general-purpose extensibility API (CreateCustomBundle) to the certificate trust configuration callback context, allowing users to register custom certificate bundle formats. This enables scenarios like Java PKCS#12 trust store generation without coupling the hosting infrastructure to specific output formats.

Changes

New public API: CertificateTrustConfigurationCallbackAnnotationContext.CreateCustomBundle

Adds an [Experimental("ASPIRECERTIFICATES001")] method on the callback context that accepts a Func<X509Certificate2Collection, CancellationToken, Task<byte[]>> factory and returns a ReferenceExpression pointing to where the generated bundle will be written. Multiple custom bundles can be registered per resource, each assigned a unique path under {rootCertificatesPath}/bundles/{bundleId}.

Example usage

builder.AddContainer("my-java-app", "my-image:latest")
    .WithCertificateTrustConfiguration(ctx =>
    {
        ctx.EnvironmentVariables["JAVAX_NET_SSL_TRUSTSTORE"] = ctx.CreateCustomBundle((certs, ct) =>
        {
            var pkcs12Builder = new Pkcs12Builder();
            var safeContents = new Pkcs12SafeContents();
            foreach (var cert in certs)
            {
                safeContents.AddCertificate(cert);
            }
            pkcs12Builder.AddSafeContentsUnencrypted(safeContents);
            pkcs12Builder.SealWithMac(string.Empty, HashAlgorithmName.SHA256, 2048);
            return Task.FromResult(pkcs12Builder.Encode());
        });
        return Task.CompletedTask;
    });

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • The PKCS#12 trust store contains only public certificates (no private keys). The default password is an empty string. This is consistent with trust-only stores and follows the same security model as the existing PEM trust bundle.
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No
  • Does the change require an update in our Aspire docs?

@danegsta danegsta requested a review from mitchdenny as a code owner February 27, 2026 00:44
Copilot AI review requested due to automatic review settings February 27, 2026 00:44
@github-actions
Copy link
Contributor

github-actions bot commented Feb 27, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 14756

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 14756"

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 pull request adds support for generating PKCS#12 format certificate trust store bundles to enable Java-based services (e.g., Kafka, Elasticsearch) to consume trusted CA certificates via javax.net.ssl.trustStore. The implementation follows the existing tracked-reference pattern used for HTTPS termination certificates, ensuring the PKCS#12 trust store is only generated when a resource's configuration callback actually references the bundle path.

Changes:

  • Added PKCS#12 bundle path and password properties to certificate trust configuration contexts, enabling Java services to reference trust stores in their preferred format
  • Implemented lazy generation of PKCS#12 trust stores using the tracked reference pattern, ensuring bundles are only created when actually needed
  • Extended both executable and container resource flows in DcpExecutor to conditionally generate and deploy truststore.p12 files when referenced
  • Added comprehensive tests covering tracked reference behavior, PKCS#12 generation, and password handling

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
src/Aspire.Hosting/ApplicationModel/CertificateTrustConfigurationCallbackAnnotation.cs Added public API properties Pkcs12BundlePath and Pkcs12BundlePassword to callback context for Java service integration
src/Aspire.Hosting/ApplicationModel/CertificateTrustExecutionConfigurationGatherer.cs Implemented tracked reference pattern for PKCS#12 bundle path and added password property to execution configuration data and context
src/Aspire.Hosting/Dcp/DcpExecutor.cs Added CreatePkcs12TrustStore helper method and integrated conditional PKCS#12 generation in both executable and container resource creation flows
tests/Aspire.Hosting.Tests/ExecutionConfigurationGathererTests.cs Added comprehensive test coverage for PKCS#12 tracking behavior, password handling, and trust store generation
Comments suppressed due to low confidence (4)

src/Aspire.Hosting/ApplicationModel/CertificateTrustConfigurationCallbackAnnotation.cs:92

  • The new public API properties Pkcs12BundlePath and Pkcs12BundlePassword need more comprehensive documentation. According to the XML documentation standards for public APIs in Aspire, you should:
  1. Add a <remarks> section explaining when and how to use these properties, particularly noting that the PKCS#12 bundle is only generated if the path is referenced
  2. Add an <example> section showing practical usage, similar to the existing examples for Arguments and EnvironmentVariables properties above (lines 37-70)
  3. For Pkcs12BundlePassword, consider documenting the default value and security considerations

Compare with existing properties like Arguments (lines 33-49) and EnvironmentVariables (lines 51-71) which include both <remarks> and <example> tags demonstrating proper public API documentation.

    /// <summary>
    /// A value provider that will resolve to a path to a PKCS#12 trust store bundle.
    /// Referencing this path in a callback will trigger generation of the PKCS#12 trust store.
    /// </summary>
    public required ReferenceExpression Pkcs12BundlePath { get; init; }

    /// <summary>
    /// The password for the PKCS#12 trust store bundle. Defaults to an empty string.
    /// </summary>
    public required string Pkcs12BundlePassword { get; init; }

src/Aspire.Hosting/ApplicationModel/CertificateTrustExecutionConfigurationGatherer.cs:223

  • The documentation for the new Pkcs12BundlePath property is too brief for a public API. According to Aspire's XML documentation standards, public APIs require:
  1. A more detailed <summary> explaining what the PKCS#12 trust store contains and its purpose (e.g., "contains trusted CA certificates in PKCS#12 format for Java-based services")
  2. A <remarks> section explaining the lazy generation behavior (only generated when referenced) and providing usage context
  3. An <example> section showing how to use this with Java services (e.g., setting javax.net.ssl.trustStore environment variables)

For reference, see the existing CertificateBundlePath property documentation pattern, but note that this is a new format that requires more explanation than the PEM bundle which is more commonly known.

    /// <summary>
    /// The path to the PKCS#12 trust store bundle file in the resource context (e.g., container filesystem).
    /// Only generated if a resource's certificate trust configuration callback references this path.
    /// </summary>
    public required ReferenceExpression Pkcs12BundlePath { get; init; }

src/Aspire.Hosting/ApplicationModel/CertificateTrustExecutionConfigurationGatherer.cs:228

  • The Pkcs12BundlePassword property documentation should be more comprehensive for a public API. Consider:
  1. Explaining why the default is an empty string (common for trust-only stores)
  2. Clarifying that this password protects the file but the certificates inside are public (no private keys)
  3. Adding a <remarks> section with security context

Compare with line 315 in ContainerFileSystemCallbackAnnotation.cs which provides a <summary> that explains when the password might be null. This property should similarly explain the default behavior and security implications.

    /// <summary>
    /// The password for the PKCS#12 trust store bundle. Defaults to an empty string.
    /// </summary>
    public string Pkcs12BundlePassword { get; init; } = string.Empty;

src/Aspire.Hosting/Dcp/DcpExecutor.cs:2919

  • While this is an internal method, the documentation is overly verbose according to Aspire's documentation standards. Internal APIs should have brief, concise <summary> tags only. The current documentation includes details that would be appropriate for a public API but are unnecessary for internal code.

Consider simplifying to something like:

/// <summary>
/// Creates a PKCS#12 trust store from the specified certificates (public keys only).
/// </summary>

The detailed explanation about Java compatibility and javax.net.ssl.trustStore usage is implementation detail that doesn't need to be in the XML docs for an internal method.

    /// <summary>
    /// Creates a PKCS#12 trust store containing the specified certificates (public keys only, no private keys).
    /// The resulting trust store is compatible with Java's keytool and can be used as a javax.net.ssl.trustStore.
    /// </summary>
    internal static byte[] CreatePkcs12TrustStore(X509Certificate2Collection certificates, string password)

@github-actions
Copy link
Contributor

github-actions bot commented Feb 27, 2026

🎬 CLI E2E Test Recordings

The following terminal recordings are available for commit 6bdfb0f:

Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AgentInitCommand_WithMalformedMcpJson_ShowsErrorAndExitsNonZero ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
CreateAndDeployToDockerCompose ▶️ View Recording
CreateAndDeployToDockerComposeInteractive ▶️ View Recording
CreateAndPublishToKubernetes ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateEmptyAppHostProject ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateStartWaitAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ▶️ View Recording
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording
TypeScriptAppHostWithProjectReferenceIntegration ▶️ View Recording

📹 Recordings uploaded automatically from CI run #22732155180

@danegsta
Copy link
Member Author

@marshalhayes helped verify against a real live Java project; this should allow for enabling certificate trust for Java services in the community toolkit as part of 13.2.

@danegsta
Copy link
Member Author

danegsta commented Mar 5, 2026

I've updated this PR to be a more generic bundle format extension implementation instead of hardcoding a new type. A new CreateCustomBundle method on the WithCertificateTrustConfigurationCallback adds a factory to create a custom bundle in any format. A test case is added to demonstrate generating a pkcs12 format bundle using this new API.

@danegsta
Copy link
Member Author

danegsta commented Mar 5, 2026

@davidfowl I switched this up to a generic approach that'll be extensible for arbitrary future bundle types.

Copy link
Member

@JamesNK JamesNK left a comment

Choose a reason for hiding this comment

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

Thanks for adding the extensible CreateCustomBundle API — the design of keeping PKCS#12 details out of the hosting infrastructure and letting consumers bring their own format is clean.

I found a few issues to address:

Bugs

  1. Executable path mismatch: In DcpExecutor for executables, custom bundle files are written to certificatesRootDir/{bundleId}, but the reference expression points to {RootCertificatesPath}/bundles/{bundleId}. The bundles/ subdirectory is missing from the write path and the directory isn't created. This means the env var will reference a non-existent path. (The container path is correct.)

  2. IsContainer is never set: The CertificateTrustConfigurationCallbackAnnotationContext.IsContainer property is never initialized in the gatherer, so it's always null. The IsContainer == true condition in CreateCustomBundle is thus always false. Additionally, the branches appear inverted — for containers you should use forward slashes (not Path.Join which gives backslashes on Windows).

Cleanup

  1. Dead TrackedReference class: Added to CertificateTrustExecutionConfigurationData but never instantiated anywhere. Appears to be leftover from a prior approach.

  2. Unnecessary System.Security.Cryptography.Pkcs dependency on Aspire.Hosting: No file under src/Aspire.Hosting/ imports this namespace. The PKCS#12 logic is entirely in the consumer's callback. This package reference should be on the test project if needed, not on the hosting package.

  3. RootCertificatesPath is nullable but used without null guard: Consider making it required string or adding a null check.

Nits

  1. Minor style: List<ContainerFileSystemEntry>? with new() — either remove the ? or defer allocation. Use .Count > 0 instead of .Any().
  2. XML doc on CreateCustomBundle says "return the bundle name and contents" but only byte[] (contents) is returned. Needs <remarks> and <example> per repo doc standards.

@dotnet-policy-service dotnet-policy-service bot added the needs-author-action An issue or pull request that requires more info or actions from the author. label Mar 5, 2026
@dotnet-policy-service dotnet-policy-service bot removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Mar 5, 2026
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.

3 participants