diff --git a/source/Calamari.Common/FeatureToggles/OctopusFeatureToggle.cs b/source/Calamari.Common/FeatureToggles/OctopusFeatureToggle.cs
index f0066c6da8..988d7e61c2 100644
--- a/source/Calamari.Common/FeatureToggles/OctopusFeatureToggle.cs
+++ b/source/Calamari.Common/FeatureToggles/OctopusFeatureToggle.cs
@@ -12,6 +12,7 @@ public static class KnownSlugs
public const string EnableLegacyKubernetesResourceChecks = "enable-legacy-kubernetes-resource-checks";
public const string AzureWebAppIgnorePreservePathsFeatureToggle = "azure-web-app-ignore-preserve-paths";
public const string AzureWebAppIgnoreChecksumFeatureToggle = "azure-web-app-ignore-checksum";
+ public const string JavaKeystoreNativeBouncyCastle = "java-keystore-native-bouncycastle";
};
public static readonly OctopusFeatureToggle KustomizePatchImageUpdatesFeatureToggle = new(KnownSlugs.KustomizePatchImageUpdatesFeatureToggle);
@@ -20,6 +21,7 @@ public static class KnownSlugs
public static readonly OctopusFeatureToggle EnableLegacyKubernetesResourceChecksFeatureToggle = new(KnownSlugs.EnableLegacyKubernetesResourceChecks);
public static readonly OctopusFeatureToggle AzureWebAppIgnorePreservePathsFeatureToggle = new(KnownSlugs.AzureWebAppIgnorePreservePathsFeatureToggle);
public static readonly OctopusFeatureToggle AzureWebAppIgnoreChecksumFeatureToggle = new(KnownSlugs.AzureWebAppIgnoreChecksumFeatureToggle);
+ public static readonly OctopusFeatureToggle JavaKeystoreNativeBouncyCastleFeatureToggle = new(KnownSlugs.JavaKeystoreNativeBouncyCastle);
public class OctopusFeatureToggle
{
diff --git a/source/Calamari.Shared/Integration/Certificates/Java/JavaKeystoreBuilder.cs b/source/Calamari.Shared/Integration/Certificates/Java/JavaKeystoreBuilder.cs
new file mode 100644
index 0000000000..5da1d0b4c2
--- /dev/null
+++ b/source/Calamari.Shared/Integration/Certificates/Java/JavaKeystoreBuilder.cs
@@ -0,0 +1,242 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using Calamari.Common.Commands;
+using Calamari.Common.Plumbing.Logging;
+using Org.BouncyCastle.Asn1.Sec;
+using Org.BouncyCastle.Asn1.X9;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.OpenSsl;
+using Org.BouncyCastle.Pkcs;
+using Org.BouncyCastle.Security;
+using Org.BouncyCastle.X509;
+using Polly;
+using Polly.Retry;
+
+namespace Calamari.Integration.Certificates.Java
+{
+ ///
+ /// Builds a PKCS#12 keystore from a PEM-encoded private key and certificate chain, using
+ /// BouncyCastle.Cryptography, without requiring a JVM or the Octopus.Dependencies.Java tool
+ /// package.
+ ///
+ /// This replicates the behaviour of the "Deploy a Keystore to the Filesystem" step's existing
+ /// implementation (com.octopus.calamari.keystore.KeystoreConfig, in the Octopus.Dependencies.Java
+ /// repository), with one deliberate difference: that implementation builds a JKS keystore via the
+ /// JDK's own built-in security provider (BouncyCastle there is only used to parse the incoming PEM
+ /// material) - JKS has no equivalent writer in .NET, so this produces a PKCS#12 keystore instead.
+ /// PKCS#12 is accepted anywhere JKS currently is, provided the consuming Tomcat/WildFly
+ /// configuration is told the keystore type explicitly (both currently rely on an unstated JKS
+ /// default).
+ ///
+ public class JavaKeystoreBuilder
+ {
+ public const string DefaultAlias = "octopus";
+ public const string DefaultPassword = "changeit";
+
+ static readonly Regex BeginPrivateKey = new Regex(@"-+BEGIN\s+.*PRIVATE\s+KEY", RegexOptions.IgnoreCase);
+
+ readonly ILog log;
+
+ public JavaKeystoreBuilder(ILog log)
+ {
+ this.log = log;
+ }
+
+ ///
+ /// Builds a PKCS#12 store in memory containing the given private key and certificate chain
+ /// under the given alias, protected by the given password. The key entry's password and the
+ /// store's own password are always the same value - Tomcat, in particular, does not support a
+ /// mismatch between the two.
+ ///
+ public Pkcs12Store BuildPkcs12Store(string alias, string privateKeyPem, string certificateChainPem, string password)
+ {
+ var fixedAlias = string.IsNullOrWhiteSpace(alias) ? DefaultAlias : alias;
+ var fixedPassword = string.IsNullOrWhiteSpace(password) ? DefaultPassword : password;
+
+ var privateKey = ParsePrivateKey(privateKeyPem);
+ var certificateChain = ParseCertificateChain(certificateChainPem);
+
+ var store = new Pkcs12StoreBuilder().Build();
+ var certificateEntries = certificateChain.Select(c => new X509CertificateEntry(c)).ToArray();
+ store.SetKeyEntry(fixedAlias, new AsymmetricKeyEntry(privateKey), certificateEntries);
+
+ return store;
+ }
+
+ ///
+ /// Builds a PKCS#12 store (see ) and writes it to disk, retrying
+ /// on failure with an exponential backoff (matching the retry policy used by the existing
+ /// Java-based implementation: 5 attempts total, starting at 5 seconds and doubling).
+ ///
+ /// The absolute path the keystore was written to.
+ public string SaveKeystoreToFile(string alias, string privateKeyPem, string certificateChainPem, string password, string keystoreFilename)
+ {
+ if (string.IsNullOrWhiteSpace(keystoreFilename))
+ throw new CommandException("The keystore filename must be supplied.");
+
+ if (!Path.IsPathRooted(keystoreFilename))
+ throw new CommandException("The keystore filename must be an absolute path.");
+
+ var fixedPassword = string.IsNullOrWhiteSpace(password) ? DefaultPassword : password;
+
+ // Parsing the key/certificate is deterministic - a malformed PEM will fail exactly the
+ // same way on every attempt, so it's built once, outside the retry pipeline below. Only
+ // the file write itself (which can hit transient permission/disk contention) is retried.
+ var store = BuildPkcs12Store(alias, privateKeyPem, certificateChainPem, fixedPassword);
+
+ CreateRetryPipeline().Execute(() =>
+ {
+ using (var fileStream = new FileStream(keystoreFilename, FileMode.Create))
+ {
+ store.Save(fileStream, fixedPassword.ToCharArray(), new SecureRandom());
+ }
+
+ // The save operation may not fail even if something prevented the file from actually
+ // being created, so check for its existence explicitly.
+ if (!File.Exists(keystoreFilename))
+ throw new CommandException($"File was not created at {keystoreFilename}");
+
+ log.Verbose($"Successfully created keystore at {keystoreFilename}");
+ });
+
+ return Path.GetFullPath(keystoreFilename);
+ }
+
+ ResiliencePipeline CreateRetryPipeline()
+ {
+ return new ResiliencePipelineBuilder()
+ .AddRetry(new RetryStrategyOptions
+ {
+ ShouldHandle = new PredicateBuilder().Handle(),
+ MaxRetryAttempts = 4,
+ Delay = TimeSpan.FromSeconds(5),
+ BackoffType = DelayBackoffType.Exponential,
+ OnRetry = args =>
+ {
+ log.Verbose($"Failed to create the keystore file, waiting {args.RetryDelay.TotalSeconds}s before trying again. {args.Outcome.Exception?.Message}");
+ return default;
+ }
+ })
+ .Build();
+ }
+
+ ///
+ /// Returns "RSA", "EC" or "DSA" for the given PEM-encoded private key - used by callers (such
+ /// as the Tomcat certificate configurator) that need to know the key's algorithm to write it
+ /// into a config file correctly, without exposing BouncyCastle's own key-parameter types.
+ ///
+ public static string GetPrivateKeyAlgorithm(string privateKeyPem)
+ {
+ var key = ParsePrivateKey(privateKeyPem);
+ switch (key)
+ {
+ case ECPrivateKeyParameters _:
+ return "EC";
+ case DsaPrivateKeyParameters _:
+ return "DSA";
+ case RsaKeyParameters _:
+ return "RSA";
+ default:
+ throw new CommandException($"Unrecognised private key algorithm: {key.GetType().Name}.");
+ }
+ }
+
+ 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.");
+
+ var trimmed = pem.Substring(match.Index);
+
+ object pemObject;
+ try
+ {
+ using (var reader = new StringReader(trimmed))
+ {
+ pemObject = new PemReader(reader).ReadObject();
+ }
+ }
+ catch (Exception ex)
+ {
+ // BouncyCastle's PemReader expects an EC private key to carry its public point
+ // alongside it. Octopus sometimes supplies EC private keys in isolation, so fall back
+ // to manually parsing the SEC1 structure when the standard parser can't handle it.
+ var fallback = TryParseEcPrivateKeyWithoutPublicPoint(trimmed);
+ if (fallback == null)
+ throw new CommandException("Unable to parse the private key.", ex);
+
+ return fallback;
+ }
+
+ switch (pemObject)
+ {
+ case AsymmetricCipherKeyPair keyPair:
+ return keyPair.Private;
+ case AsymmetricKeyParameter keyParameter:
+ return keyParameter;
+ default:
+ throw new CommandException($"Unrecognised private key format: {pemObject?.GetType().Name ?? "null"}.");
+ }
+ }
+
+ static AsymmetricKeyParameter? TryParseEcPrivateKeyWithoutPublicPoint(string pem)
+ {
+ if (!pem.Contains("EC PRIVATE KEY"))
+ return null;
+
+ try
+ {
+ using (var reader = new StringReader(pem))
+ {
+ var pemObject = new Org.BouncyCastle.Utilities.IO.Pem.PemReader(reader).ReadPemObject();
+ var ecPrivateKeyStructure = ECPrivateKeyStructure.GetInstance(pemObject.Content);
+
+ var parametersObject = ecPrivateKeyStructure.GetParameters();
+ var curveParameters = parametersObject is Org.BouncyCastle.Asn1.DerObjectIdentifier namedCurveOid
+ ? Org.BouncyCastle.Asn1.X9.ECNamedCurveTable.GetByOid(namedCurveOid)
+ : Org.BouncyCastle.Asn1.X9.X9ECParameters.GetInstance(parametersObject);
+
+ return new ECPrivateKeyParameters(
+ ecPrivateKeyStructure.GetKey(),
+ new Org.BouncyCastle.Crypto.Parameters.ECDomainParameters(curveParameters.Curve, curveParameters.G, curveParameters.N, curveParameters.H, curveParameters.GetSeed()));
+ }
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ static IList ParseCertificateChain(string pem)
+ {
+ var certificates = new List();
+
+ // The certificate PEM may contain a chain (leaf followed by one or more intermediates)
+ // concatenated together. X509CertificateParser.ReadCertificate can be called repeatedly
+ // against the same stream to read each one in turn until the stream is exhausted.
+ using (var stream = new MemoryStream(Encoding.ASCII.GetBytes(pem)))
+ {
+ var parser = new X509CertificateParser();
+ while (true)
+ {
+ var certificate = parser.ReadCertificate(stream);
+ if (certificate == null)
+ break;
+
+ certificates.Add(certificate);
+ }
+ }
+
+ if (!certificates.Any())
+ throw new CommandException("Certificate file does not contain any certificates. This is probably because the input certificate file is invalid.");
+
+ return certificates;
+ }
+ }
+}
diff --git a/source/Calamari.Tests/Java/Fixtures/JavaKeystoreActionFixture.cs b/source/Calamari.Tests/Java/Fixtures/JavaKeystoreActionFixture.cs
new file mode 100644
index 0000000000..1959ca070f
--- /dev/null
+++ b/source/Calamari.Tests/Java/Fixtures/JavaKeystoreActionFixture.cs
@@ -0,0 +1,117 @@
+using System.IO;
+using Calamari.Common.Commands;
+using Calamari.Common.FeatureToggles;
+using Calamari.Common.Features.Processes;
+using Calamari.Common.Plumbing.Variables;
+using Calamari.Deployment;
+using Calamari.Deployment.Features.Java;
+using Calamari.Deployment.Features.Java.Actions;
+using Calamari.Testing.Helpers;
+using FluentAssertions;
+using NSubstitute;
+using NUnit.Framework;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.OpenSsl;
+using Org.BouncyCastle.Security;
+
+namespace Calamari.Tests.Java.Fixtures
+{
+ [TestFixture]
+ public class JavaKeystoreActionFixture
+ {
+ const string CertificateVariableName = "MyCertificate";
+
+ ICommandLineRunner commandLineRunner;
+ InMemoryLog log;
+ string workingDirectory;
+ string privateKeyPem;
+ string certificatePem;
+
+ [SetUp]
+ public void SetUp()
+ {
+ commandLineRunner = Substitute.For();
+ commandLineRunner.Execute(Arg.Any()).Returns(new CommandResult("", 0));
+ log = new InMemoryLog();
+ workingDirectory = Path.Combine(Path.GetTempPath(), "JavaKeystoreActionFixture-" + System.Guid.NewGuid());
+ Directory.CreateDirectory(workingDirectory);
+
+ var keyGenerator = new RsaKeyPairGenerator();
+ keyGenerator.Init(new KeyGenerationParameters(new SecureRandom(), 2048));
+ var keyPair = keyGenerator.GenerateKeyPair();
+
+ var generator = new Org.BouncyCastle.X509.X509V3CertificateGenerator();
+ var subject = new Org.BouncyCastle.Asn1.X509.X509Name("CN=octopus-action-test");
+ generator.SetSerialNumber(Org.BouncyCastle.Math.BigInteger.ValueOf(System.DateTime.UtcNow.Ticks));
+ generator.SetIssuerDN(subject);
+ generator.SetSubjectDN(subject);
+ generator.SetNotBefore(System.DateTime.UtcNow.AddDays(-1));
+ generator.SetNotAfter(System.DateTime.UtcNow.AddYears(1));
+ generator.SetPublicKey(keyPair.Public);
+ var certificate = generator.Generate(new Org.BouncyCastle.Crypto.Operators.Asn1SignatureFactory("SHA256WITHRSA", keyPair.Private));
+
+ using (var writer = new StringWriter())
+ {
+ new PemWriter(writer).WriteObject(keyPair.Private);
+ privateKeyPem = writer.ToString();
+ }
+
+ using (var writer = new StringWriter())
+ {
+ new PemWriter(writer).WriteObject(certificate);
+ certificatePem = writer.ToString();
+ }
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ if (Directory.Exists(workingDirectory))
+ Directory.Delete(workingDirectory, true);
+ }
+
+ RunningDeployment BuildDeployment(bool nativeToggleEnabled, string keystoreFilename)
+ {
+ var variables = new CalamariVariables();
+ variables.Set(SpecialVariables.Action.Java.JavaKeystore.Variable, CertificateVariableName);
+ variables.Set(SpecialVariables.Action.Java.JavaKeystore.Password, Some.String());
+ variables.Set(SpecialVariables.Action.Java.JavaKeystore.KeystoreFilename, keystoreFilename);
+ variables.Set(SpecialVariables.Action.Java.JavaKeystore.KeystoreAlias, "myalias");
+ variables.Set(SpecialVariables.Certificate.PrivateKeyPem(CertificateVariableName), privateKeyPem);
+ variables.Set(SpecialVariables.Certificate.CertificatePem(CertificateVariableName), certificatePem);
+ variables.Set(SpecialVariables.Certificate.Subject(CertificateVariableName), "CN=octopus-action-test");
+
+ if (nativeToggleEnabled)
+ variables.Set(KnownVariables.EnabledFeatureToggles, OctopusFeatureToggles.KnownSlugs.JavaKeystoreNativeBouncyCastle);
+
+ return new RunningDeployment(variables);
+ }
+
+ [Test]
+ public void Execute_WhenNativeToggleEnabled_WritesKeystoreWithoutInvokingJava()
+ {
+ var keystorePath = Path.Combine(workingDirectory, "test.p12");
+ var deployment = BuildDeployment(nativeToggleEnabled: true, keystoreFilename: keystorePath);
+ var action = new JavaKeystoreAction(new JavaRunner(commandLineRunner, deployment.Variables), log);
+
+ action.Execute(deployment);
+
+ File.Exists(keystorePath).Should().BeTrue();
+ commandLineRunner.DidNotReceive().Execute(Arg.Any());
+ }
+
+ [Test]
+ public void Execute_WhenNativeToggleDisabled_FallsBackToInvokingCalamariJar()
+ {
+ var keystorePath = Path.Combine(workingDirectory, "test.p12");
+ var deployment = BuildDeployment(nativeToggleEnabled: false, keystoreFilename: keystorePath);
+ var action = new JavaKeystoreAction(new JavaRunner(commandLineRunner, deployment.Variables), log);
+
+ action.Execute(deployment);
+
+ File.Exists(keystorePath).Should().BeFalse("the legacy path only writes the keystore via the mocked-out java process, which doesn't actually run");
+ commandLineRunner.Received().Execute(Arg.Is(i => i.Arguments.Contains("com.octopus.calamari.keystore.KeystoreConfig")));
+ }
+ }
+}
diff --git a/source/Calamari.Tests/Java/Fixtures/JavaKeystoreBuilderFixture.cs b/source/Calamari.Tests/Java/Fixtures/JavaKeystoreBuilderFixture.cs
new file mode 100644
index 0000000000..df32dd91d8
--- /dev/null
+++ b/source/Calamari.Tests/Java/Fixtures/JavaKeystoreBuilderFixture.cs
@@ -0,0 +1,176 @@
+using System;
+using System.IO;
+using Calamari.Common.Commands;
+using Calamari.Integration.Certificates.Java;
+using Calamari.Testing.Helpers;
+using FluentAssertions;
+using NUnit.Framework;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.OpenSsl;
+using Org.BouncyCastle.Pkcs;
+using Org.BouncyCastle.Security;
+using Org.BouncyCastle.X509;
+using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
+
+namespace Calamari.Tests.Java.Fixtures
+{
+ [TestFixture]
+ public class JavaKeystoreBuilderFixture
+ {
+ InMemoryLog log;
+ string workingDirectory;
+
+ [SetUp]
+ public void SetUp()
+ {
+ log = new InMemoryLog();
+ workingDirectory = Path.Combine(Path.GetTempPath(), "JavaKeystoreBuilderFixture-" + Guid.NewGuid());
+ Directory.CreateDirectory(workingDirectory);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ if (Directory.Exists(workingDirectory))
+ Directory.Delete(workingDirectory, true);
+ }
+
+ [Test]
+ public void SaveKeystoreToFile_WithRsaKeyAndSingleCertificate_ProducesAValidPkcs12KeystoreContainingTheSameKeyAndCertificate()
+ {
+ var (privateKeyPem, certificatePem, keyPair, certificate) = CreateSelfSignedRsaCertificate("CN=octopus-test");
+
+ var keystorePath = Path.Combine(workingDirectory, "test.p12");
+ var builder = new JavaKeystoreBuilder(log);
+ var testPassword = Some.String();
+ var resultPath = builder.SaveKeystoreToFile("myalias", privateKeyPem, certificatePem, testPassword, keystorePath);
+
+ resultPath.Should().Be(Path.GetFullPath(keystorePath));
+ File.Exists(keystorePath).Should().BeTrue();
+
+ var reloaded = new Pkcs12StoreBuilder().Build();
+ using (var fileStream = new FileStream(keystorePath, FileMode.Open))
+ {
+ reloaded.Load(fileStream, testPassword.ToCharArray());
+ }
+
+ reloaded.ContainsAlias("myalias").Should().BeTrue();
+ reloaded.IsKeyEntry("myalias").Should().BeTrue();
+
+ var reloadedChain = reloaded.GetCertificateChain("myalias");
+ reloadedChain.Should().HaveCount(1);
+ reloadedChain[0].Certificate.CertificateStructure.Should().Be(certificate.CertificateStructure);
+
+ var reloadedKey = reloaded.GetKey("myalias").Key;
+ reloadedKey.Should().Be(keyPair.Private);
+ }
+
+ [Test]
+ public void SaveKeystoreToFile_WithBlankAliasAndPassword_FallsBackToOctopusDefaults()
+ {
+ var (privateKeyPem, certificatePem, _, _) = CreateSelfSignedRsaCertificate("CN=octopus-defaults-test");
+
+ var keystorePath = Path.Combine(workingDirectory, "defaults.p12");
+ var builder = new JavaKeystoreBuilder(log);
+
+ builder.SaveKeystoreToFile("", privateKeyPem, certificatePem, "", keystorePath);
+
+ var reloaded = new Pkcs12StoreBuilder().Build();
+ using (var fileStream = new FileStream(keystorePath, FileMode.Open))
+ {
+ reloaded.Load(fileStream, JavaKeystoreBuilder.DefaultPassword.ToCharArray());
+ }
+
+ reloaded.ContainsAlias(JavaKeystoreBuilder.DefaultAlias).Should().BeTrue();
+ }
+
+ [Test]
+ public void SaveKeystoreToFile_WithRelativeKeystorePath_ThrowsCommandException()
+ {
+ var (privateKeyPem, certificatePem, _, _) = CreateSelfSignedRsaCertificate("CN=octopus-relative-path-test");
+ var builder = new JavaKeystoreBuilder(log);
+
+ Action act = () => builder.SaveKeystoreToFile("myalias", privateKeyPem, certificatePem, Some.String(), "relative/path.p12");
+
+ act.Should().Throw().WithMessage("*absolute path*");
+ }
+
+ [Test]
+ public void SaveKeystoreToFile_WithNoCertificatesInPem_ThrowsCommandException()
+ {
+ var (privateKeyPem, _, _, _) = CreateSelfSignedRsaCertificate("CN=octopus-no-cert-test");
+ var builder = new JavaKeystoreBuilder(log);
+ var keystorePath = Path.Combine(workingDirectory, "no-cert.p12");
+
+ Action act = () => builder.SaveKeystoreToFile("myalias", privateKeyPem, "not a certificate", Some.String(), keystorePath);
+
+ act.Should().Throw().WithMessage("*does not contain any certificates*");
+ }
+
+ [Test]
+ public void BuildPkcs12Store_WithCertificateChain_IncludesEveryCertificateInOrder()
+ {
+ // A genuine chain, not two unrelated self-signed certificates: the intermediate is its
+ // own CA, and the leaf is actually issued by (signed by) that intermediate.
+ var (_, intermediateCertPem, intermediateKeyPair, intermediateCert) = CreateSelfSignedRsaCertificate("CN=intermediate");
+ var (leafKeyPem, leafCertPem, _, leafCert) = CreateRsaCertificate("CN=leaf", "CN=intermediate", intermediateKeyPair);
+
+ var builder = new JavaKeystoreBuilder(log);
+ var store = builder.BuildPkcs12Store("myalias", leafKeyPem, leafCertPem + "\n" + intermediateCertPem, Some.String());
+
+ var chain = store.GetCertificateChain("myalias");
+ chain.Should().HaveCount(2);
+ chain[0].Certificate.CertificateStructure.Should().Be(leafCert.CertificateStructure);
+ chain[1].Certificate.CertificateStructure.Should().Be(intermediateCert.CertificateStructure);
+ }
+
+ static (string PrivateKeyPem, string CertificatePem, AsymmetricCipherKeyPair KeyPair, X509Certificate Certificate) CreateSelfSignedRsaCertificate(string subjectDn)
+ {
+ var keyGenerator = new RsaKeyPairGenerator();
+ keyGenerator.Init(new KeyGenerationParameters(new SecureRandom(), 2048));
+ var keyPair = keyGenerator.GenerateKeyPair();
+ return CreateRsaCertificate(subjectDn, subjectDn, keyPair, keyPair);
+ }
+
+ static (string PrivateKeyPem, string CertificatePem, AsymmetricCipherKeyPair KeyPair, X509Certificate Certificate) CreateRsaCertificate(string subjectDn, string issuerDn, AsymmetricCipherKeyPair issuerKeyPair)
+ {
+ var keyGenerator = new RsaKeyPairGenerator();
+ keyGenerator.Init(new KeyGenerationParameters(new SecureRandom(), 2048));
+ var keyPair = keyGenerator.GenerateKeyPair();
+ return CreateRsaCertificate(subjectDn, issuerDn, keyPair, issuerKeyPair);
+ }
+
+ static (string PrivateKeyPem, string CertificatePem, AsymmetricCipherKeyPair KeyPair, X509Certificate Certificate) CreateRsaCertificate(string subjectDn, string issuerDn, AsymmetricCipherKeyPair keyPair, AsymmetricCipherKeyPair issuerKeyPair)
+ {
+ var signingKeyPair = issuerKeyPair;
+
+ var generator = new X509V3CertificateGenerator();
+ generator.SetSerialNumber(Org.BouncyCastle.Math.BigInteger.ValueOf(DateTime.UtcNow.Ticks));
+ generator.SetIssuerDN(new Org.BouncyCastle.Asn1.X509.X509Name(issuerDn));
+ generator.SetSubjectDN(new Org.BouncyCastle.Asn1.X509.X509Name(subjectDn));
+ generator.SetNotBefore(DateTime.UtcNow.AddDays(-1));
+ generator.SetNotAfter(DateTime.UtcNow.AddYears(1));
+ generator.SetPublicKey(keyPair.Public);
+
+ var signatureFactory = new Org.BouncyCastle.Crypto.Operators.Asn1SignatureFactory("SHA256WITHRSA", signingKeyPair.Private);
+ var certificate = generator.Generate(signatureFactory);
+
+ string privateKeyPem;
+ using (var writer = new StringWriter())
+ {
+ new PemWriter(writer).WriteObject(keyPair.Private);
+ privateKeyPem = writer.ToString();
+ }
+
+ string certificatePem;
+ using (var writer = new StringWriter())
+ {
+ new PemWriter(writer).WriteObject(certificate);
+ certificatePem = writer.ToString();
+ }
+
+ return (privateKeyPem, certificatePem, keyPair, certificate);
+ }
+ }
+}
diff --git a/source/Calamari/Commands/Java/JavaLibraryCommand.cs b/source/Calamari/Commands/Java/JavaLibraryCommand.cs
index 143034aec4..d9ba89bb17 100644
--- a/source/Calamari/Commands/Java/JavaLibraryCommand.cs
+++ b/source/Calamari/Commands/Java/JavaLibraryCommand.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using Calamari.Commands.Support;
using Calamari.Common.Commands;
+using Calamari.Common.FeatureToggles;
using Calamari.Common.Features.Deployment;
using Calamari.Common.Features.EmbeddedResources;
using Calamari.Common.Features.Packages.Java;
@@ -38,7 +39,15 @@ public JavaLibraryCommand(IScriptEngine scriptEngine, ICalamariFileSystem fileSy
public override int Execute(string[] commandLineArguments)
{
Options.Parse(commandLineArguments);
- JavaRuntime.VerifyExists();
+
+ // The native BouncyCastle-based keystore step doesn't need a JVM at all. Every other
+ // action type this command dispatches to (Tomcat/WildFly state and certificate steps)
+ // still shells out to calamari.jar, so the check only skips for that one action type.
+ var isNativeKeystoreStep = actionType == SpecialVariables.Action.Java.JavaKeystore.CertificateActionTypeName
+ && OctopusFeatureToggles.JavaKeystoreNativeBouncyCastleFeatureToggle.IsEnabled(variables);
+
+ if (!isNativeKeystoreStep)
+ JavaRuntime.VerifyExists();
var embeddedResources = new AssemblyEmbeddedResources();
diff --git a/source/Calamari/Deployment/Features/Java/Actions/JavaKeystoreAction.cs b/source/Calamari/Deployment/Features/Java/Actions/JavaKeystoreAction.cs
index 0e5fe36b33..392f4525af 100644
--- a/source/Calamari/Deployment/Features/Java/Actions/JavaKeystoreAction.cs
+++ b/source/Calamari/Deployment/Features/Java/Actions/JavaKeystoreAction.cs
@@ -1,7 +1,9 @@
using System.Collections.Generic;
using Calamari.Commands.Java;
using Calamari.Common.Commands;
+using Calamari.Common.FeatureToggles;
using Calamari.Common.Plumbing.Logging;
+using Calamari.Integration.Certificates.Java;
namespace Calamari.Deployment.Features.Java.Actions
{
@@ -17,18 +19,31 @@ public override void Execute(RunningDeployment deployment)
{
var variables = deployment.Variables;
log.Info("Adding certificate to Java Keystore");
-
+
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);
+ var privateKeyPem = variables.Get(SpecialVariables.Certificate.PrivateKeyPem(certificateId));
+ var certificatePem = variables.Get(SpecialVariables.Certificate.CertificatePem(certificateId));
+
+ if (OctopusFeatureToggles.JavaKeystoreNativeBouncyCastleFeatureToggle.IsEnabled(variables))
+ {
+ var keystorePath = new JavaKeystoreBuilder(log).SaveKeystoreToFile(keystoreAlias, privateKeyPem, certificatePem, password, keystoreFilename);
+ log.Info($"Keystore was successfully deployed to \"{keystorePath}\".");
+ return;
+ }
+
var envVariables = new Dictionary(){
- {"OctopusEnvironment_Java_Certificate_Variable", certificateId},
- {"OctopusEnvironment_Java_Certificate_Password", variables.Get(SpecialVariables.Action.Java.JavaKeystore.Password)},
- {"OctopusEnvironment_Java_Certificate_KeystoreFilename", variables.Get(SpecialVariables.Action.Java.JavaKeystore.KeystoreFilename)},
- {"OctopusEnvironment_Java_Certificate_KeystoreAlias", variables.Get(SpecialVariables.Action.Java.JavaKeystore.KeystoreAlias)},
- {"OctopusEnvironment_Java_Certificate_Private_Key", variables.Get(SpecialVariables.Certificate.PrivateKeyPem(certificateId))},
- {"OctopusEnvironment_Java_Certificate_Public_Key", variables.Get(SpecialVariables.Certificate.CertificatePem(certificateId))},
+ {"OctopusEnvironment_Java_Certificate_Variable", certificateId},
+ {"OctopusEnvironment_Java_Certificate_Password", password},
+ {"OctopusEnvironment_Java_Certificate_KeystoreFilename", keystoreFilename},
+ {"OctopusEnvironment_Java_Certificate_KeystoreAlias", keystoreAlias},
+ {"OctopusEnvironment_Java_Certificate_Private_Key", privateKeyPem},
+ {"OctopusEnvironment_Java_Certificate_Public_Key", certificatePem},
{"OctopusEnvironment_Java_Certificate_Public_Key_Subject", variables.Get(SpecialVariables.Certificate.Subject(certificateId))},
};
runner.Run("com.octopus.calamari.keystore.KeystoreConfig", envVariables);
}
}
-}
\ No newline at end of file
+}