Skip to content

Add BouncyCastle-based native keystore builder for the Java Keystore step - #2140

Open
zentron wants to merge 2 commits into
mainfrom
feature/java-keystore-bouncycastle
Open

Add BouncyCastle-based native keystore builder for the Java Keystore step#2140
zentron wants to merge 2 commits into
mainfrom
feature/java-keystore-bouncycastle

Conversation

@zentron

@zentron zentron commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

⚠️ Does this change require a corresponding Server Change? Yes. The java-keystore-native-bouncycastle toggle needs a matching OctopusFeatureToggle definition on the Server side before it can ever be enabled — not included in this PR. Adding the Requires Server Change label.

Background

The "Deploy a Keystore to the Filesystem" step currently shells out to com.octopus.calamari.keystore.KeystoreConfig, a Kotlin process bundled in the Octopus.Dependencies.Java NuGet package (unmaintained since 2022, ships a static 2017 JDK8 tools.jar).

This is the first step in an effort to drop that dependency from Calamari's Java/Tomcat/WildFly steps, none of which actually need a JVM once the process-based tooling is replaced.

Results

  • New JavaKeystoreBuilder (Calamari.Shared/Integration/Certificates/Java/) parses a PEM private key + cert chain with BouncyCastle.Cryptography and builds a PKCS#12 keystore, gated behind the java-keystore-native-bouncycastle toggle (off by default; existing Java-based path unchanged).
  • PKCS#12, not JKS — JKS support in the original comes from the JDK's own built-in provider, which has no .NET equivalent. PKCS#12 is accepted anywhere JKS is today, but downstream config (Tomcat cert step, WildFly) needs to declare the keystore type explicitly instead of relying on an unstated JKS default. Handled for Tomcat in the follow-up PR; WildFly is separate, later work.
  • Manual SEC1 fallback for EC private keys supplied without an embedded public point, mirroring a real workaround in the original Kotlin code — BouncyCastle's standard PemReader can't handle that case.
  • File-write retry (5 attempts, 5s exponential backoff, matching the original's Spring RetryTemplate policy) wraps only the file write, not PEM parsing — a malformed cert fails identically every attempt, so retrying it is pure waste.
  • Wired into JavaKeystoreAction (branches on toggle) and JavaLibraryCommand (skips JavaRuntime.VerifyExists() only for this action type when the toggle is on).

Testing

7 new NUnit tests (JavaKeystoreBuilderFixture, JavaKeystoreActionFixture): real BouncyCastle-generated RSA certs round-tripped through a real Pkcs12Store reload, and asserting the Java command-line runner is never invoked when the toggle is on. No JVM available in the dev sandbox to exercise the existing path side-by-side; the 4 pre-existing DeployJavaArchiveFixture failures (need a real JVM) are untouched and unrelated.

How to review

Core logic is JavaKeystoreBuilder.cs — the PEM parsing (including the EC fallback) is the riskiest part. Everything else is toggle plumbing.

…step

Replaces the com.octopus.calamari.keystore.KeystoreConfig Java process with
a native BouncyCastle.Cryptography implementation, gated behind the
java-keystore-native-bouncycastle feature toggle so the existing Java-based
path remains the default.

Builds a PKCS12 keystore rather than JKS (the original's JKS support comes
from the JDK's own provider, which has no .NET equivalent; BouncyCastle
there is PEM-parsing only). PKCS12 is accepted anywhere JKS currently is,
provided the consuming Tomcat/WildFly config declares the keystore type
explicitly.
@gitguardian

gitguardian Bot commented Aug 13, 2026

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Copilot AI left a comment

Copy link
Copy Markdown

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 introduces a JVM-free implementation of the “Deploy a Keystore to the Filesystem” Java keystore step by building a PKCS#12 keystore directly in .NET using BouncyCastle, gated behind a new java-keystore-native-bouncycastle feature toggle (off by default). This is part of the broader effort to remove the unmaintained Octopus.Dependencies.Java tooling dependency from Calamari’s Java-related steps.

Changes:

  • Added JavaKeystoreBuilder (BouncyCastle-based) to build and persist PKCS#12 keystores natively, including retry logic around the file write.
  • Wired JavaKeystoreAction to branch between the legacy Java process path and the new native path based on the feature toggle.
  • Updated JavaLibraryCommand to skip JavaRuntime.VerifyExists() only when executing the keystore action with the native toggle enabled; added NUnit coverage for the new builder/action behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
source/Calamari/Deployment/Features/Java/Actions/JavaKeystoreAction.cs Branches between native keystore build vs legacy Java runner based on the toggle.
source/Calamari/Commands/Java/JavaLibraryCommand.cs Skips JVM existence check only for the native keystore action when toggle enabled.
source/Calamari.Shared/Integration/Certificates/Java/JavaKeystoreBuilder.cs New native PKCS#12 keystore builder + PEM parsing + retry-on-write implementation.
source/Calamari.Common/FeatureToggles/OctopusFeatureToggle.cs Adds the java-keystore-native-bouncycastle toggle slug and definition.
source/Calamari.Tests/Java/Fixtures/JavaKeystoreBuilderFixture.cs Tests PKCS#12 round-trip and default alias/password behavior.
source/Calamari.Tests/Java/Fixtures/JavaKeystoreActionFixture.cs Verifies native toggle path does not invoke the Java command runner.
Suppressed comments (2)

source/Calamari/Deployment/Features/Java/Actions/JavaKeystoreAction.cs:28

  • The native keystore path passes privateKeyPem/certificatePem directly into the BouncyCastle parser. If either variable is missing, the parser will currently throw ArgumentNullException rather than a CommandException with a helpful message. Fetch these as mandatory variables (consistent with other Calamari commands) so failures are clear and actionable.
            var privateKeyPem = variables.Get(SpecialVariables.Certificate.PrivateKeyPem(certificateId));
            var certificatePem = variables.Get(SpecialVariables.Certificate.CertificatePem(certificateId));

source/Calamari.Shared/Integration/Certificates/Java/JavaKeystoreBuilder.cs:218

  • ParseCertificateChain will throw ArgumentNullException if the PEM string is null (e.g., missing variable). Add an explicit null/whitespace check and throw CommandException so callers get a clear error message.
        static IList<X509Certificate> ParseCertificateChain(string pem)
        {
            var certificates = new List<X509Certificate>();

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 23 to +26
var certificateId = variables.Get(SpecialVariables.Action.Java.JavaKeystore.Variable);
var password = variables.Get(SpecialVariables.Action.Java.JavaKeystore.Password);
var keystoreFilename = variables.Get(SpecialVariables.Action.Java.JavaKeystore.KeystoreFilename);
var keystoreAlias = variables.Get(SpecialVariables.Action.Java.JavaKeystore.KeystoreAlias);
Comment on lines +149 to +154
static AsymmetricKeyParameter ParsePrivateKey(string pem)
{
var match = BeginPrivateKey.Match(pem);
if (!match.Success)
throw new CommandException("The private key does not contain a recognisable PEM private key block.");

return new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder().Handle<Exception>(),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants