Skip to content

Create the temporary PKCS12 with FIPS-approved algorithms - #483

Open
miharp wants to merge 3 commits into
puppetlabs:mainfrom
miharp:fips-pkcs12
Open

miharp wants to merge 3 commits into
puppetlabs:mainfrom
miharp:fips-pkcs12

Conversation

@miharp

@miharp miharp commented Oct 23, 2026 •

Copy link
Copy Markdown

Summary

On a host running in FIPS mode, importing a private key and certificate fails because OpenSSL::PKCS12.create is called with its default arguments. Under OpenSSL 1.1.1 in FIPS mode (Enterprise Linux 8) the default bag encryption is RC2-40 and 3DES and the call raises encrypt error; under an OpenSSL 3 whose FIPS provider has no PKCS12 KDF (for example the one bundled with the FIPS builds of the OpenVox agent) the integrity MAC cannot be derived and it raises mac generation error. In both cases ensure => latest fails with Could not set 'latest' on ensure.

This change adds a pkcs12_create helper to the keytool provider. When OpenSSL.fips_mode is true it asks for PBES2 with AES-256 for both bags (the OpenSSL 3 default, readable by keytool since Java 8u301 and 11.0.12) and skips the MAC; the PKCS12 is a temporary file that keytool reads once, so the MAC adds nothing. Outside FIPS mode the call is unchanged, so hosts whose OpenSSL and Java already agree on a format keep it. Unit tests cover both branches; the README gains a FIPS note under Limitations.

Additional Context

  • Root cause and the steps to reproduce.
    • AlmaLinux 8 container, OPENSSL_FORCE_FIPS_MODE=1, system Ruby: OpenSSL::PKCS12.create(pass, name, key, cert, []) raises PKCS12Error: encrypt error; with 'AES-256-CBC', 'AES-256-CBC', nil, -1 it succeeds and keytool -importkeystore accepts the file.
    • AlmaLinux 9 in FIPS mode with the FIPS build of the OpenVox agent (openvox-agent 9.1.1 redhatfips9, bundled OpenSSL 3.5.9, OpenSSL.fips_mode true): java_ks from main fails with Could not set 'latest' on ensure: mac generation error for both the default (PKCS12) target keystore and storetype => jks; with this branch both imports succeed and a second run reports no changes. Same result with openvox-agent 8.31.1 (OpenSSL 3.0.22).
    • Red Hat's system OpenSSL 3 carries a PKCS12 KDF exception for FIPS mode, so a system Ruby on EL9 does not reproduce the MAC failure; agents with a bundled OpenSSL do.
  • Thought process behind the implementation.
    • The only FIPS-sensitive step in the provider is building the PKCS12 for -importkeystore; keytool itself and the SHA-256 fingerprint comparison (Support for systems running in FIPS mode #160) are fine. The explicit algorithms and the missing MAC are confined to FIPS mode (second commit): with OpenSSL 3 they are the defaults anyway, and on a non-FIPS OpenSSL 1.x host forcing PBES2 would change a working file into one an older keytool may not read, which matters because ensure => latest removes the existing alias before the import.
    • Red Hat's FIPS-configured OpenJDK 21 still accepts JKS keystores with private keys, so no change to the default store type is needed.

Related Issues (if any)

#160 is the only earlier FIPS change (MD5 to SHA-256 fingerprints, 2016). No open issue describes this failure.

Checklist

  • 🟢 Spec tests. (rake spec: 69 examples, 0 failures; rubocop clean)
  • 🟢 Acceptance tests. (not run; no FIPS host in the acceptance matrix)
  • Manually verified. (puppet apply on a FIPS-mode AlmaLinux 9 host with the OpenVox FIPS agent, PKCS12 and JKS target keystores, idempotent second run)

Assisted by Claude.

miharp and others added 2 commits October 22, 2026 10:20
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 <mike@mikeharp.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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 <mike@mikeharp.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@CLAassistant

CLAassistant commented Oct 27, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

span786
span786 previously approved these changes Nov 3, 2026

@span786 span786 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @miharp, this is a well-scoped fix with a very clear write-up. Keeping the explicit algorithms and the missing MAC to FIPS mode only is the right call, given that ensure => latest deletes the alias before re-importing.

I checked the FIPS branch on its own, outside your branch, with Ruby 3.2.9 / OpenSSL 3.6 and OpenSSL::PKCS12.create(pass, name, key, cert, [], 'AES-256-CBC', 'AES-256-CBC', nil, -1):

  • openssl pkcs12 -info shows both bags as PBES2, PBKDF2, AES-256-CBC and MAC is absent. nil for key_iter keeps the default iteration count, and -1 for mac_iter is OpenSSL's "no MAC" sentinel, as intended.
  • OpenSSL::PKCS12.new(der, pass) re-parses it, and the key and cert round-trip.
  • keytool (Java 8u421) imports it with the provider's exact -importkeystore -srcstoretype PKCS12 … -alias arguments, into both a JKS and a PKCS12 destination, and -list shows a PrivateKeyEntry.

Approving. Two things are needed before this can merge, neither of them code: mark the PR ready for review (it's still a draft), and sign the CLA (license/cla is pending).

Non-blocking suggestion: both new specs mock OpenSSL::PKCS12.create, so they'd still pass if the argument order were wrong (e.g. -1 landing in key_iter). A real-crypto example would catch that. The ASN.1 check works because a PFX with a MAC has 3 top-level elements and one without has 2. I checked this standalone:

      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

CI: the only failure is Spec tests (Puppet: ~> 9.0, Ruby Ver: 4) failing with Could not find gem 'puppet (~> 9.0)'. That's because fork PRs don't get PUPPET_FORGE_TOKEN, so it's infra, not this change. Puppet 8 specs are green. Acceptance didn't run (fork gate), and as you note, the matrix has no FIPS host, so it would only show that the unchanged non-FIPS path still works.

@span786 span786 self-assigned this Nov 3, 2026
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 <noreply@anthropic.com>
@miharp
miharp marked this pull request as ready for review November 11, 2026 12:37
@miharp
miharp requested a review from a team as a code owner November 11, 2026 12:37

@span786 span786 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @miharp, all three points from my earlier review are covered: the PR is out of draft, the CLA is signed, and 9d75800 adds the real-crypto example. The new commit only touches keytool_spec.rb, and the example matches the one I checked standalone, so the "no macData" assertion and the key round-trip both test the real OpenSSL::PKCS12.create call rather than a mock. The provider change is the same as when I approved it.

Re-approving on the new head, since the push dismissed the earlier approval.

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