Repository navigation
Conversation
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>
span786
left a comment
There was a problem hiding this comment.
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 -infoshows both bags asPBES2, PBKDF2, AES-256-CBCandMAC is absent.nilforkey_iterkeeps the default iteration count, and-1formac_iteris 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 … -aliasarguments, into both a JKS and a PKCS12 destination, and-listshows aPrivateKeyEntry.
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)
endCI: 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.
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>
There was a problem hiding this comment.
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.
Summary
On a host running in FIPS mode, importing a private key and certificate fails because
OpenSSL::PKCS12.createis 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 raisesencrypt 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 raisesmac generation error. In both casesensure => latestfails withCould not set 'latest' on ensure.This change adds a
pkcs12_createhelper to the keytool provider. WhenOpenSSL.fips_modeis true it asks for PBES2 with AES-256 for both bags (the OpenSSL 3 default, readable bykeytoolsince Java 8u301 and 11.0.12) and skips the MAC; the PKCS12 is a temporary file thatkeytoolreads 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
OPENSSL_FORCE_FIPS_MODE=1, system Ruby:OpenSSL::PKCS12.create(pass, name, key, cert, [])raisesPKCS12Error: encrypt error; with'AES-256-CBC', 'AES-256-CBC', nil, -1it succeeds andkeytool -importkeystoreaccepts the file.OpenSSL.fips_modetrue):java_ksfrommainfails withCould not set 'latest' on ensure: mac generation errorfor both the default (PKCS12) target keystore andstoretype => 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).-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 becauseensure => latestremoves the existing alias before the import.Related Issues (if any)
#160 is the only earlier FIPS change (MD5 to SHA-256 fingerprints, 2016). No open issue describes this failure.
Checklist
rake spec: 69 examples, 0 failures; rubocop clean)puppet applyon a FIPS-mode AlmaLinux 9 host with the OpenVox FIPS agent, PKCS12 and JKS target keystores, idempotent second run)Assisted by Claude.