From 9917dcd46741d39e01e17fc1a35fa8ad8079a10a Mon Sep 17 00:00:00 2001 From: Michael Harp Date: Thu, 22 Oct 2026 07:57:30 -0400 Subject: [PATCH 1/3] Create the temporary PKCS12 with FIPS-approved algorithms On a host running in FIPS mode, OpenSSL::PKCS12.create with its default arguments fails with "encrypt error" under OpenSSL 1.1.1 (Enterprise Linux 8), because the default bag encryption is RC2-40 and 3DES. The PKCS12 integrity MAC is keyed with the PKCS12 KDF, which the OpenSSL 3 FIPS provider does not implement either. The provider never sets these arguments, so java_ks cannot import a private key and certificate on such hosts. Ask for PBES2 with AES-256 explicitly, which is the OpenSSL 3 default and is read by keytool since Java 8u301 and 11.0.12, and skip the MAC when OpenSSL reports FIPS mode. The PKCS12 is a temporary file that keytool reads once, so the MAC adds nothing. Reproduced two ways. On AlmaLinux 8 with OPENSSL_FORCE_FIPS_MODE=1 the current call raises PKCS12Error "encrypt error" and the new call succeeds. On an AlmaLinux 9 host in FIPS mode with the FIPS build of the OpenVox agent (openvox-agent 8.31.1 redhatfips9, bundled OpenSSL 3.0.22, OpenSSL.fips_mode true) the module on main fails with "mac generation error", and with this change `puppet apply` imports the key into a JKS and into a PKCS12 keystore and the second run reports no changes. Red Hat's system OpenSSL 3 carries a PKCS12 KDF exception, so on AlmaLinux 9 with system Ruby both calls succeed. Signed-off-by: Michael Harp Co-Authored-By: Claude Fable 5.1 --- README.md | 2 ++ lib/puppet/provider/java_ks/keytool.rb | 22 ++++++++++++++++++- .../puppet/provider/java_ks/keytool_spec.rb | 18 +++++++++++++-- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b46cb10..527512f 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ For information on the classes and types, see the [REFERENCE.md](https://github. The java_ks module uses the `keytool` and `openssl` commands. It should work on all systems with these commands. +On a host running in FIPS mode, the module creates the temporary PKCS12 file it hands to `keytool` with PBES2 and AES-256 and without an integrity MAC, because the PKCS12 defaults in OpenSSL 1.x (RC2 and 3DES) and the key derivation for the PKCS12 MAC are not FIPS approved. Without this, importing a private key fails with `encrypt error` (OpenSSL 1.1.1) or `mac generation error` (OpenSSL 3 whose FIPS provider has no PKCS12 KDF, such as the one bundled with the FIPS builds of the OpenVox agent). + Java 7 is supported as of 1.0.0. Developed against IBM Java 6 on AIX. Other versions may be unsupported. diff --git a/lib/puppet/provider/java_ks/keytool.rb b/lib/puppet/provider/java_ks/keytool.rb index 59832a0..4f4059a 100644 --- a/lib/puppet/provider/java_ks/keytool.rb +++ b/lib/puppet/provider/java_ks/keytool.rb @@ -30,10 +30,30 @@ def to_pkcs12(path) chain_certs = get_chain(certificate) x509_cert = chain_certs.shift end - pkcs12 = OpenSSL::PKCS12.create(password, @resource[:name], pkey, x509_cert, chain_certs) + pkcs12 = pkcs12_create(password, @resource[:name], pkey, x509_cert, chain_certs) File.open(path, 'wb') { |f| f.print pkcs12.to_der } end + # OpenSSL's PKCS12 defaults do not work on a host running in FIPS mode. + # OpenSSL 1.x encrypts the bags with RC2-40 and 3DES, which FIPS mode refuses, + # and the PKCS12 integrity MAC is keyed with the PKCS12 KDF, which the OpenSSL 3 + # FIPS provider does not implement. Ask for PBES2 with AES-256 explicitly (the + # OpenSSL 3 default, and readable by keytool since Java 8u301 and 11.0.12) and + # skip the MAC in FIPS mode. The file only exists long enough for keytool to + # read it once, so the MAC adds nothing. + def pkcs12_create(pass, name, pkey, cert, chain) + mac_iter = nil + if fips_mode? + Puppet.debug("java_ks: OpenSSL is in FIPS mode, creating the PKCS12 for #{name} without a MAC") + mac_iter = -1 + end + OpenSSL::PKCS12.create(pass, name, pkey, cert, chain, 'AES-256-CBC', 'AES-256-CBC', nil, mac_iter) + end + + def fips_mode? + OpenSSL.respond_to?(:fips_mode) && OpenSSL.fips_mode + end + # Keytool can only import a jceks keystore if the format is der. Generating and # importing a keystore is used to add private_key and certificate pairs. def to_der(path) diff --git a/spec/unit/puppet/provider/java_ks/keytool_spec.rb b/spec/unit/puppet/provider/java_ks/keytool_spec.rb index 47ba664..e50c1f6 100755 --- a/spec/unit/puppet/provider/java_ks/keytool_spec.rb +++ b/spec/unit/puppet/provider/java_ks/keytool_spec.rb @@ -133,7 +133,7 @@ pkcs_double = BogusPkcs.new expect(pkcs_double).to receive(:to_der) - expect(OpenSSL::PKCS12).to receive(:create).with(resource[:password], resource[:name], 'priv_obj', 'cert_obj', []).and_return(pkcs_double) + expect(OpenSSL::PKCS12).to receive(:create).with(resource[:password], resource[:name], 'priv_obj', 'cert_obj', [], 'AES-256-CBC', 'AES-256-CBC', nil, nil).and_return(pkcs_double) provider.to_pkcs12("#{temp_dir}testing.stuff") end # rubocop:enable RSpec/MultipleExpectations @@ -159,7 +159,7 @@ pkcs_double = BogusPkcs.new expect(pkcs_double).to receive(:to_der) - expect(OpenSSL::PKCS12).to receive(:create).with(resource[:password], resource[:name], 'priv_obj', 'cert_obj', []).and_return(pkcs_double) + expect(OpenSSL::PKCS12).to receive(:create).with(resource[:password], resource[:name], 'priv_obj', 'cert_obj', [], 'AES-256-CBC', 'AES-256-CBC', nil, nil).and_return(pkcs_double) provider.to_pkcs12("#{temp_dir}testing.stuff") end # rubocop:enable RSpec/MultipleExpectations @@ -167,6 +167,20 @@ end end + describe '#pkcs12_create' do + it 'uses PBES2 with AES-256 and keeps the MAC outside FIPS mode' do + allow(provider).to receive(:fips_mode?).and_return(false) + expect(OpenSSL::PKCS12).to receive(:create).with('puppet', 'app', 'priv_obj', 'cert_obj', [], 'AES-256-CBC', 'AES-256-CBC', nil, nil) + provider.pkcs12_create('puppet', 'app', 'priv_obj', 'cert_obj', []) + end + + it 'skips the MAC in FIPS mode' do + allow(provider).to receive(:fips_mode?).and_return(true) + expect(OpenSSL::PKCS12).to receive(:create).with('puppet', 'app', 'priv_obj', 'cert_obj', [], 'AES-256-CBC', 'AES-256-CBC', nil, -1) + provider.pkcs12_create('puppet', 'app', 'priv_obj', 'cert_obj', []) + end + end + describe '#import_ks' do it 'executes openssl and keytool with specific options' do expect(provider).to receive(:to_pkcs12).with("#{temp_dir}testing.stuff") From 16baec2d3fcfb22bd4d45660110acac12de1315b Mon Sep 17 00:00:00 2001 From: Michael Harp Date: Fri, 23 Oct 2026 07:26:46 -0400 Subject: [PATCH 2/3] Keep the default PKCS12 algorithms outside FIPS mode The first version asked for PBES2 with AES-256 on every host. That is a no-op with OpenSSL 3, whose default it is, but on a non-FIPS host with OpenSSL 1.x it changed the format of a file that older keytools may not read (PBES2 support arrived in Java 8u301 and 11.0.12), and ensure => latest removes the existing alias before the import, so a failed import would also drop the working entry. The explicit algorithms and the missing MAC are now confined to FIPS mode, where the defaults cannot work at all. Signed-off-by: Michael Harp Co-Authored-By: Claude Fable 5.1 --- README.md | 2 +- lib/puppet/provider/java_ks/keytool.rb | 20 +++++++++---------- .../puppet/provider/java_ks/keytool_spec.rb | 10 +++++----- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 527512f..be20e91 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ For information on the classes and types, see the [REFERENCE.md](https://github. The java_ks module uses the `keytool` and `openssl` commands. It should work on all systems with these commands. -On a host running in FIPS mode, the module creates the temporary PKCS12 file it hands to `keytool` with PBES2 and AES-256 and without an integrity MAC, because the PKCS12 defaults in OpenSSL 1.x (RC2 and 3DES) and the key derivation for the PKCS12 MAC are not FIPS approved. Without this, importing a private key fails with `encrypt error` (OpenSSL 1.1.1) or `mac generation error` (OpenSSL 3 whose FIPS provider has no PKCS12 KDF, such as the one bundled with the FIPS builds of the OpenVox agent). +On a host running in FIPS mode, the module creates the temporary PKCS12 file it hands to `keytool` with PBES2 and AES-256 and without an integrity MAC, because the PKCS12 defaults in OpenSSL 1.x (RC2 and 3DES) and the key derivation for the PKCS12 MAC are not FIPS approved. Without this, importing a private key fails with `encrypt error` (OpenSSL 1.1.1) or `mac generation error` (OpenSSL 3 whose FIPS provider has no PKCS12 KDF, such as the one bundled with the FIPS builds of the OpenVox agent). `keytool` reads PBES2 since Java 8u301 and 11.0.12. Outside FIPS mode the file is created as before. Java 7 is supported as of 1.0.0. diff --git a/lib/puppet/provider/java_ks/keytool.rb b/lib/puppet/provider/java_ks/keytool.rb index 4f4059a..36405d3 100644 --- a/lib/puppet/provider/java_ks/keytool.rb +++ b/lib/puppet/provider/java_ks/keytool.rb @@ -37,17 +37,17 @@ def to_pkcs12(path) # OpenSSL's PKCS12 defaults do not work on a host running in FIPS mode. # OpenSSL 1.x encrypts the bags with RC2-40 and 3DES, which FIPS mode refuses, # and the PKCS12 integrity MAC is keyed with the PKCS12 KDF, which the OpenSSL 3 - # FIPS provider does not implement. Ask for PBES2 with AES-256 explicitly (the - # OpenSSL 3 default, and readable by keytool since Java 8u301 and 11.0.12) and - # skip the MAC in FIPS mode. The file only exists long enough for keytool to - # read it once, so the MAC adds nothing. + # FIPS provider does not implement. In FIPS mode, ask for PBES2 with AES-256 + # explicitly (the OpenSSL 3 default, and readable by keytool since Java 8u301 + # and 11.0.12) and skip the MAC; the file only exists long enough for keytool + # to read it once, so the MAC adds nothing. Outside FIPS mode the call is + # unchanged, so the file keeps whatever format the host's OpenSSL and Java + # already agreed on. def pkcs12_create(pass, name, pkey, cert, chain) - mac_iter = nil - if fips_mode? - Puppet.debug("java_ks: OpenSSL is in FIPS mode, creating the PKCS12 for #{name} without a MAC") - mac_iter = -1 - end - OpenSSL::PKCS12.create(pass, name, pkey, cert, chain, 'AES-256-CBC', 'AES-256-CBC', nil, mac_iter) + return OpenSSL::PKCS12.create(pass, name, pkey, cert, chain) unless fips_mode? + + Puppet.debug("java_ks: OpenSSL is in FIPS mode, creating the PKCS12 for #{name} with AES-256 and without a MAC") + OpenSSL::PKCS12.create(pass, name, pkey, cert, chain, 'AES-256-CBC', 'AES-256-CBC', nil, -1) end def fips_mode? diff --git a/spec/unit/puppet/provider/java_ks/keytool_spec.rb b/spec/unit/puppet/provider/java_ks/keytool_spec.rb index e50c1f6..7c6d50d 100755 --- a/spec/unit/puppet/provider/java_ks/keytool_spec.rb +++ b/spec/unit/puppet/provider/java_ks/keytool_spec.rb @@ -133,7 +133,7 @@ pkcs_double = BogusPkcs.new expect(pkcs_double).to receive(:to_der) - expect(OpenSSL::PKCS12).to receive(:create).with(resource[:password], resource[:name], 'priv_obj', 'cert_obj', [], 'AES-256-CBC', 'AES-256-CBC', nil, nil).and_return(pkcs_double) + expect(OpenSSL::PKCS12).to receive(:create).with(resource[:password], resource[:name], 'priv_obj', 'cert_obj', []).and_return(pkcs_double) provider.to_pkcs12("#{temp_dir}testing.stuff") end # rubocop:enable RSpec/MultipleExpectations @@ -159,7 +159,7 @@ pkcs_double = BogusPkcs.new expect(pkcs_double).to receive(:to_der) - expect(OpenSSL::PKCS12).to receive(:create).with(resource[:password], resource[:name], 'priv_obj', 'cert_obj', [], 'AES-256-CBC', 'AES-256-CBC', nil, nil).and_return(pkcs_double) + expect(OpenSSL::PKCS12).to receive(:create).with(resource[:password], resource[:name], 'priv_obj', 'cert_obj', []).and_return(pkcs_double) provider.to_pkcs12("#{temp_dir}testing.stuff") end # rubocop:enable RSpec/MultipleExpectations @@ -168,13 +168,13 @@ end describe '#pkcs12_create' do - it 'uses PBES2 with AES-256 and keeps the MAC outside FIPS mode' do + it 'keeps the default algorithms outside FIPS mode' do allow(provider).to receive(:fips_mode?).and_return(false) - expect(OpenSSL::PKCS12).to receive(:create).with('puppet', 'app', 'priv_obj', 'cert_obj', [], 'AES-256-CBC', 'AES-256-CBC', nil, nil) + expect(OpenSSL::PKCS12).to receive(:create).with('puppet', 'app', 'priv_obj', 'cert_obj', []) provider.pkcs12_create('puppet', 'app', 'priv_obj', 'cert_obj', []) end - it 'skips the MAC in FIPS mode' do + it 'uses AES-256 and skips the MAC in FIPS mode' do allow(provider).to receive(:fips_mode?).and_return(true) expect(OpenSSL::PKCS12).to receive(:create).with('puppet', 'app', 'priv_obj', 'cert_obj', [], 'AES-256-CBC', 'AES-256-CBC', nil, -1) provider.pkcs12_create('puppet', 'app', 'priv_obj', 'cert_obj', []) From 9d75800ee88a060f5e43519d522d15e7ccd431a2 Mon Sep 17 00:00:00 2001 From: Michael Harp Date: Wed, 11 Nov 2026 07:35:10 -0500 Subject: [PATCH 3/3] Check the FIPS PKCS12 with real crypto The two existing examples mock OpenSSL::PKCS12.create, so they would still pass if the arguments were in the wrong order (for example -1 landing in key_iter instead of mac_iter). Build a real key and certificate, create the PKCS12 through the FIPS branch, and check that the PFX has no macData and that the key round-trips. Suggested by span786 in review. Co-Authored-By: Claude Fable 5.2 --- .../puppet/provider/java_ks/keytool_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/unit/puppet/provider/java_ks/keytool_spec.rb b/spec/unit/puppet/provider/java_ks/keytool_spec.rb index 7c6d50d..dc3806a 100755 --- a/spec/unit/puppet/provider/java_ks/keytool_spec.rb +++ b/spec/unit/puppet/provider/java_ks/keytool_spec.rb @@ -179,6 +179,24 @@ expect(OpenSSL::PKCS12).to receive(:create).with('puppet', 'app', 'priv_obj', 'cert_obj', [], 'AES-256-CBC', 'AES-256-CBC', nil, -1) provider.pkcs12_create('puppet', 'app', 'priv_obj', 'cert_obj', []) end + + it 'produces a PKCS12 without a MAC that round-trips in FIPS mode' do + allow(provider).to receive(:fips_mode?).and_return(true) + key = OpenSSL::PKey::RSA.new(2048) + cert = OpenSSL::X509::Certificate.new + cert.version = 2 + cert.serial = 1 + cert.subject = cert.issuer = OpenSSL::X509::Name.parse('/CN=app') + cert.public_key = key.public_key + cert.not_before = Time.now + cert.not_after = Time.now + 3600 + cert.sign(key, OpenSSL::Digest.new('SHA256')) + + der = provider.pkcs12_create('puppet', 'app', key, cert, []).to_der + + expect(OpenSSL::ASN1.decode(der).value.size).to eq(2) # version + authSafe, no macData + expect(OpenSSL::PKCS12.new(der, 'puppet').key.to_der).to eq(key.to_der) + end end describe '#import_ks' do