Skip to content

[#1056] Add the RSASVE/KDF-SHA256/AESKWP key wrapping transformation for runtimes without RSA-OAEP - #1194

Open
maximthomas wants to merge 24 commits into
OpenIdentityPlatform:masterfrom
maximthomas:1056-rsasve-key-transport
Open

maximthomas wants to merge 24 commits into
OpenIdentityPlatform:masterfrom
maximthomas:1056-rsasve-key-transport

Conversation

@maximthomas

@maximthomas maximthomas commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1056.

Summary

A Java runtime whose cryptography comes from a SunPKCS11 provider alone, such as SunPKCS11-NSS-FIPS on a Linux system in FIPS mode, offers no RSA-OAEP. Its only RSA padding for key transport is PKCS#1 v1.5, which NIST SP 800-131A Rev. 2 disallows. On such a runtime the crypto manager has no acceptable value for key-wrapping-transformation.

This PR adds one: RSASVE/KDF-SHA256/AESKWP. It is a key wrapping scheme of OpenDJ's own, built on the hybrid key transport of NIST SP 800-56B Rev. 2, section 9.3. It needs only RSA/ECB/NoPadding, SHA-256 and AES-KWP from the runtime. The scheme is opt-in only: the default stays RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING, and neither setup nor the server selects the new value by itself.

The scheme

A new package-private class, org.opends.server.crypto.RsaSveKeyTransport, wraps a secret key in three steps:

Step Standard What it does
RSASVE.GENERATE SP 800-56B Rev. 2, 7.2.1 Draws z uniformly in [2, n-2] by rejection sampling over nLen random bytes. Raw RSA of I2OS(z, nLen) gives C, normalized to exactly nLen bytes.
One-step KDF SP 800-56C Rev. 2, 4.1 KWK = SHA-256(00000001 ‖ Z ‖ FixedInfo), used as an AES-256 key.
AES-KWP SP 800-38F W = KWP(KWK, secret key). Asks for AESWrapPad first, the only name Bouncy Castle FIPS knows, so that Bouncy Castle FIPS does the key wrap when it is installed first; then AES/KWP/NoPadding. SunJCE and SunPKCS11 register AESWrapPad as an alias of their AES/KWP/NoPadding.

Before the wrap, the certificate of the receiving server is checked the way Cipher.init(WRAP_MODE, certificate) checks it for RSA-OAEP: a critical key usage without keyEncipherment is refused.

The wrapped key is len16(C) ‖ C ‖ W. It goes, hex-encoded, into the fourth field of the existing ds-cfg-symmetric-key value wrappingKeyID:transformation:algorithm:hexWrappedKey. The value format does not change.

FixedInfo is length-prefixed (a 4-byte big-endian length before each variable field) and binds, in order:

  • the canonical scheme name as AlgorithmID;
  • an empty PartyUInfo;
  • the wrapping key ID (field 1 of the stored value) as PartyVInfo;
  • the wrapped key's algorithm (field 3);
  • L = 256.

So none of the fields stored next to the wrapped key can be changed without the unwrap failing.

No nonce. Keys are wrapped for a server from its published certificate alone, without that server taking part, so no nonce of the receiving server enters the derivation. The scheme is therefore "RSASVE + one-step KDF + AES-KWP", built on section 9.3 but not the KAS1-basic scheme to the letter. The documentation says so, and leaves to each deployment's FIPS 140-3 assessment whether it counts as an approved key transport method there.

Unwrap. It splits len16(C) ‖ C ‖ W. Before the private key decrypts anything, it refuses an instance key pair without its private key or without an RSA public key, and a C which is not on exactly nLen bytes (RSASVE.RECOVER) or is not an integer with 1 < c < n − 1 (RSADP). The modulus comes from the instance key certificate, because a private key held by a PKCS#11 token does not always implement RSAKey. It then raw-RSA-decrypts C and left-pads the result to nLen bytes, because Bouncy Castle FIPS drops leading zero bytes. Last, it derives the KWK and KWP-unwraps W. Both ciphers are asked for, and the raw RSA cipher is initialized with the private key, before the decryption, since none of this depends on the value: a runtime which lacks a cipher is reported naming it, and a provider which cannot use the private key with its own reason, not as a value which cannot be unwrapped. Every failure from the decryption on is reported as one InvalidKeyException, RSASVE/KDF-SHA256/AESKWP cannot unwrap the key, with the original exception as its cause. So how a value is refused does not depend on what the private key recovered from it. Bad keys and malformed values are reported only as GeneralSecurityException subtypes, the only type CryptoManagerImpl catches around the wrap and the unwrap.

Crypto manager

CryptoManagerImpl routes to the new class by the transformation name, matched case-insensitively like a JCE transformation, in three places:

  • isKeyWrappingTransformationSupported checks that the runtime offers the three algorithms. A missing one is refused through the existing ERR_CRYPTOMGR_KEY_WRAPPING_TRANSFORMATION_UNSUPPORTED, which names the property. The existing trial wrap that follows it then runs the whole RSASVE wrap at configuration change and start time.
  • encodeSymmetricKeyAttribute wraps with the scheme, passing the certificate so that its key usage is checked.
  • decodeSymmetricKeyAttribute unwraps by the name each stored value records. Existing RSA-OAEP values keep decoding as before. It reads the instance key certificate once: the instance key ID and, on the scheme's branch, the public key both come from it.

No message and no message ordinal is added. ConfigureDS, the installer and the setup warning are unchanged.

Topology

A server without this change that meets a value wrapped with the scheme fails in Cipher.getInstance. So does a server with this change on a runtime without one of the three algorithms, such as Java 11 without Bouncy Castle FIPS, which has no AES-KWP: since it does not use the scheme itself, it refuses nothing. Both report ERR_CRYPTOMGR_DECODE_SYMMETRIC_KEY_ATTRIBUTE_DECIPHER, which CryptoManagerSync only logs; the second names the missing algorithm. The property description and the install guide therefore say that every server of the replication topology must run a version supporting the scheme, on a runtime which offers the three algorithms, before any server is switched to it.

Documentation

  • The description of key-wrapping-transformation in CryptoManagerConfiguration.xml, shown by dsconfig help and the generated configuration reference.
  • The FIPS install procedure, [#install-fips] in chap-install.adoc. It corrects what a SunPKCS11 runtime offers (RSA/ECB/PKCS1Padding and RSA/ECB/NoPadding), describes the scheme, and uses it in the changes.ldif example.

Both say that the SunPKCS11 provider of OpenJDK 17 offers no AES-KWP, so that the server refuses the value on an OpenJDK 17 runtime with that provider alone, and that the provider offers it from OpenJDK 19 on (P11KeyWrapCipher is absent from Temurin 17.0.7 and 17.0.11, present in 19.0.1, 21.0.2, 24.0.2, 25.0.2 and Zulu 26).

Tests

  • RsaSveKeyTransportTestCase (new, 45 cases):

    • round trips of five keys from 8 to 32 bytes (DES, AES-128, HmacSHA1, DESede, AES-256), including the 20-byte HmacSHA1 key that needs KWP's padding;
    • a known-answer test pinning the stored layout, the KDF and the KWP output, whose vector was computed independently of the implementation, for the key wrap of both the JDK and Bouncy Castle FIPS;
    • rejection of secret values outside [2, n-2];
    • a secret value with a leading zero byte under Bouncy Castle FIPS;
    • a 1024-bit modulus;
    • another private key and changed stored fields failing the unwrap;
    • malformed values refused with InvalidKeyException;
    • encapsulated values of another length than the modulus, or outside 1 < c < n − 1, refused before the decryption under both the JDK providers and Bouncy Castle FIPS;
    • one message for every unwrap failure after the decryption;
    • a provider which drops the leading zero bytes of the encapsulated value: the value is still stored on the modulus length;
    • the support check and the unwrap naming the missing key wrap;
    • the unwrap reporting the RSA cipher's own refusal of the private key, not "cannot unwrap the key";
    • an instance key pair without its private key or with a non-RSA public key refused before any cipher;
    • the key wrap served by Bouncy Castle FIPS when it is installed first, even where the JDK offers AES/KWP/NoPadding.

    The round trips of the five keys run under both the JDK providers and Bouncy Castle FIPS (AESWrapPad only, and a raw RSA that drops leading zeros).

  • CryptoManagerTestCase (+8 cases):

    • the scheme is accepted under both spellings;
    • the scheme is refused, naming key-wrapping-transformation and AES/KWP/NoPadding, on a runtime without AES-KWP;
    • a key generated while it is configured is published under its name, unwraps, and decrypts the data it encrypted;
    • each value is unwrapped by the transformation it records: an RSA-OAEP value while the scheme is configured, and the scheme's value, also with its name in lower case, once RSA-OAEP is configured again;
    • a secret key is re-wrapped for another instance only where its certificate's key usage allows it: a critical key usage without keyEncipherment is refused under both RSA-OAEP and the scheme, while one with keyEncipherment, or one that is not critical, is accepted.

    The cases which need AES-KWP run with Bouncy Castle FIPS first, so that JDK 11 has it.

  • TestCaseUtils gets two fixtures: withBcFipsFirst installs Bouncy Castle FIPS first and puts the providers back afterwards (the existing approved-only fixture of CryptoManagerTestCase now uses it), and withoutKeyWrap runs an action on a runtime without AES-KWP, as Java 11 without Bouncy Castle FIPS is.

  • The CryptoManagerTestCase case which generates a key wrapped with the scheme deletes it afterwards, so that no later restart of the test instance meets a value a Java 11 runtime cannot unwrap.

Local runs:

Class JDK 26.0.1 JDK 11.0.25
RsaSveKeyTransportTestCase 45/0/0/0 45/0/0/10 (the JDK-provider rows and the 1024-bit case need AES/KWP/NoPadding, which JDK 11 lacks)
CryptoManagerTestCase 58/0/0/0 (50 before) 58/0/0/0
GetSymmetricKeyExtendedOperationTestCase 2/0/0/0 2/0/0/0

Results are tests/failures/errors/skipped.

Outstanding: one manual run on a SunPKCS11-only runtime

No CI cell runs a runtime whose only provider is SunPKCS11. One manual run on a RHEL FIPS-mode OpenJDK 17+ with SunPKCS11-NSS-FIPS only is still outstanding:

The runtime's SunPKCS11 has to offer AES/KWP/NoPadding. In the upstream OpenJDK sources, 17.0.7 and 17.0.11 register no AES-KWP in SunPKCS11, while 19.0.1 and later do; a RHEL build may differ. On a runtime without it, the server refuses the value at step 1 and names the property.

  1. Set key-wrapping-transformation to RSASVE/KDF-SHA256/AESKWP and start the server. This also exercises the wrap side through the trial wrap.
  2. Make the server generate a secret key: an encrypted backup, or a backend with confidentiality enabled.
  3. Restart. Check that no ERR_TRUSTSTORESYNC_EXCEPTION appears and that the data still decrypts. This is the only check of the unwrap side, which no configuration-time check covers: a raw RSA decrypt with the token-held private key, the KWP unwrap, and the import of the unwrapped key.
  4. Add a second server, to exercise the re-wrap for another instance.

If step 3 fails, data under that key cannot be read until the server runs on a runtime that can unwrap it. The wrapped value stays in cn=secret keys, so this is recoverable.

Also unverified without a token: wrapping a sensitive PKCS#11 secret key, whose getEncoded() returns null, relies on the provider's C_WrapKey.

@maximthomas maximthomas added enhancement docs tests Test suites: fixing, enabling, un-disabling security Security fixes / CodeQL code-scanning alerts fips FIPS 140 mode: BC-FIPS provider, BCFKS keystores, approved algorithms labels Oct 14, 2026
Comment thread opendj-server-legacy/src/main/java/org/opends/server/crypto/RsaSveKeyTransport.java Dismissed
Comment thread opendj-server-legacy/src/main/java/org/opends/server/crypto/RsaSveKeyTransport.java Dismissed
Comment thread opendj-server-legacy/src/main/java/org/opends/server/crypto/RsaSveKeyTransport.java Dismissed
final byte[] wrapped = new byte[buffer.remaining()];
buffer.get(wrapped);

final Cipher rsa = Cipher.getInstance(RSA_TRANSFORMATION);
Comment thread opendj-server-legacy/src/main/java/org/opends/server/crypto/RsaSveKeyTransport.java Dismissed
…st the modulus before decrypting it

RsaSveKeyTransport.unwrap took the length of the encapsulated value from
its two-byte header and checked only that the value fit in the wrapped
key. A value shorter or longer than the modulus reached the raw RSA
decryption, and the failures that followed differed by what the private
key recovered: toLength's "value is longer than the RSA modulus", the
key wrap's own text, or, under Bouncy Castle FIPS, an
ArrayIndexOutOfBoundsException that escaped the GeneralSecurityException
the crypto manager catches.

unwrap now refuses, before the private key decrypts anything, a value
which is not on as many bytes as the modulus (RSASVE.RECOVER) or not an
integer c with 1 < c < n - 1 (RSADP), and reports every failure from the
decryption on as one InvalidKeyException, "cannot unwrap the key", with
the original exception as its cause.

The modulus comes from the instance key pair's public key, which
decodeSymmetricKeyAttribute reads from the instance key certificate: a
private key held by a PKCS#11 token does not always implement RSAKey.
…heme as SP 800-56B section 9.3 in its Javadoc

Commit 75e8f99 reworded the install guide and the
key-wrapping-transformation description only; the opening sentence of
the RsaSveKeyTransport Javadoc still called the scheme the hybrid key
transport of section 9.3 itself.
…ion back re-wraps no secret key

The secret keys wrapped while RSASVE/KDF-SHA256/AESKWP was configured
stay wrapped with it after the property is set back: the crypto manager
wraps only new keys, the trial wrap and the keys it re-wraps for another
server. A server which later loses the scheme still cannot read them.
…E naming the property

Both RSASVE cases of CryptoManagerTestCase ran with Bouncy Castle FIPS
first, where the runtime offers every algorithm of the scheme, and
asserted acceptance only. Without the support check in
isKeyWrappingTransformationSupported, a runtime lacking AES-KWP would
be refused by the trial wrap with
ERR_CRYPTOMGR_CANNOT_GET_PREFERRED_KEY_WRAPPING_CIPHER, which does not
name key-wrapping-transformation, though the install guide says the
refusal does.
…ed by the transformation it records

The only RSASVE decode of the suite ran while RSASVE was also the
configured transformation, on a value naming it in upper case, and
every other decode ran under RSA-OAEP on RSA-OAEP values. A decode
choosing the unwrap by the configured transformation, or matching the
scheme name with case, stayed green.

testRsaSveWrappedKeyIsPublishedAndUnwraps now also decodes a value
wrapped with RSA-OAEP while RSASVE is configured, and the RSASVE value,
as stored and with the scheme name in lower case, once RSA-OAEP is
configured again.
…sulated value on the modulus length

SunJCE and Bouncy Castle FIPS both return the raw RSA encryption on as
many bytes as the modulus, so dropping the toLength normalisation of
wrap() stayed green. A provider returning it without its leading zero
bytes would then store a header one byte short for about one key in
256, which unwrap() now refuses.

A stand-in raw RSA cipher which drops leading zero bytes wraps a secret
value whose encapsulation starts with a zero byte; the header must be
the modulus length and the key must unwrap.
…ovider

testUnwrapFailureHasOneMessage ran Bouncy Castle FIPS and then the JDK
providers in one case, so on Java 11, which offers no AES/KWP/NoPadding,
the skip of the JDK part reported the Bouncy Castle FIPS part as skipped
too. Each provider is now a row of its own, as in the round trips.
…fore an RSASVE wrap

The RSA-OAEP wrap passes the instance certificate to Cipher.init, which
refuses a certificate whose critical key usage does not allow
keyEncipherment. The RSASVE wrap took the public key alone, so it
wrapped secret keys for a certificate restricted to signing, such as
one published in cn=instance keys by another tool.

RsaSveKeyTransport.wrap now takes the certificate and checks its key
usage the way Cipher.init does before wrapping with its public key.
testSecretKeyIsWrappedOnlyWhereTheKeyUsageAllowsIt re-wraps a secret
key for an instance certificate with each key usage: a critical one
without keyEncipherment is refused by both transformations, one with
keyEncipherment or one which is not critical is not.
… must offer the RSASVE algorithms

The install guide and the key-wrapping-transformation description gave
the OpenDJ version as the only thing to check on every server before
any server uses the RSASVE scheme. A server on Java 11 without Bouncy
Castle FIPS runs a supporting version but has no AES-KWP: it cannot
unwrap the keys the others wrap with the scheme, and since it does not
use the scheme itself it refuses nothing and only logs an error for
each key.

Both now ask for a Java runtime which offers the three algorithms too,
and say what such a server does.
…wrap when it is first

The key wrap asked for AES/KWP/NoPadding first and AESWrapPad second.
Bouncy Castle FIPS knows AES-KWP only as AESWrapPad, so in the FIPS
mode of OpenDJ, which installs it first but keeps SunJCE, the key wrap
of a Java 17+ runtime went to SunJCE, outside the FIPS module, while
the raw RSA and the key derivation went to Bouncy Castle FIPS.

AESWrapPad is now asked for first. SunJCE and SunPKCS11 register it as
an alias of their AES/KWP/NoPadding, so a runtime without Bouncy Castle
FIPS first gets the same cipher as before, and both implementations
produce the same wrapped bytes. A runtime which knows neither name is
still refused naming AES/KWP/NoPadding.
…wrap cannot get it

unwrap() asked for the raw RSA and the AES-KWP ciphers inside the try
which reports every failure as "cannot unwrap the key", so a server on
a runtime without AES-KWP, such as Java 11 without Bouncy Castle FIPS,
logged each key wrapped with the scheme as one which cannot be
unwrapped, as tampered or foreign data would be.

Neither cipher depends on the value, so both are now asked for before
the try: a missing one is reported naming it, before anything is
decrypted, and what the private key recovers still shows in no message.
…sable instance key pair

No case unwrapped with a key pair without its private key or with a
public key which is not an RSA key: every unwrap passed an RSA pair
with its private key. A cast in place of getModulus() threw a
ClassCastException, which escapes the catch of the crypto manager, and
stayed green; without the null guard only the message changed.

testUnusableInstanceKeyPairIsRefusedWithASecurityException checks both
refusals by their messages. Both come before any cipher, so the case
runs on Java 11 too.
@maximthomas
maximthomas marked this pull request as ready for review October 15, 2026 08:39
@maximthomas
maximthomas requested a review from vharseko October 15, 2026 08:39

@vharseko vharseko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks — the scheme reads well, and I could check it from the description alone: building FixedInfo, the one-step KDF and AES-KWP from the PR text (not from the code) gives the testKnownAnswer vector A29EDD3A…AF5 with SunJCE on 17.0.20 and 21.0.12, and BC-FIPS 2.1.3 AESWrapPad produces the same bytes as SunJCE AES/KWP/NoPadding, padding case included. The points listed under Outstanding (the SunPKCS11 manual run, no nonce, no unwrap-side configuration check) are not re-raised here.

Should fix

1. RsaSveKeyTransport.unwrap: rsa.init(DECRYPT_MODE, privateKey) is still inside the catch-all (RsaSveKeyTransport.java:258)

4352233 moved Cipher.getInstance out of the try because neither cipher depends on the value. rsa.init does not depend on it either: it only takes the instance private key. Inside the try, a provider that cannot use that key — for instance a SunPKCS11 runtime that refuses the private key loaded from the ads-truststore, which is what step 3 of the outstanding manual run exercises — is reported as RSASVE/KDF-SHA256/AESKWP cannot unwrap the key, exactly like a forged value. The real reason does not reach the error log either: ERR_CRYPTOMGR_DECODE_SYMMETRIC_KEY_ATTRIBUTE_DECIPHER carries getExceptionMessage(ex), which drops the cause, and CryptoManagerSync logs only e.getMessage() (post-response path) or stackTraceToSingleLineString(e), which lists causes only in a debug build. Calling rsa.init before the try keeps the one-message rule for everything that depends on the value; no test pins the current placement.

Worth doing

2. The install guide does not say that SunPKCS11 on Java 17 has no AES-KWP (chap-install.adoc:849)

The procedure targets a SunPKCS11-only runtime and now uses RSASVE/KDF-SHA256/AESKWP in its changes.ldif (line 876). Upstream JDK 17 has no AES-KWP in SunPKCS11 — sun.security.pkcs11.P11KeyWrapCipher is absent from 17.0.20 and present in 21.0.12 — so on OpenJDK 17, a common RHEL FIPS runtime, the server refuses the value at start. The paragraph says in general terms that the runtime has to offer the three algorithms, and the PR description states the 17 vs 19+ difference, but the guide only gives Java 11 without BC-FIPS as an example. One sentence there (and in the property description) naming Java 17 with SunPKCS11 would spare an admin following the procedure a failed start.

Nits

  • 3. testRsaSveWrappedKeyIsPublishedAndUnwraps (CryptoManagerTestCase.java:992) leaves its AES/CBC/PKCS5Padding-192 key, wrapped with RSASVE, in cn=secret keys. The admin backend is config/admin-backend.ldif and survives restartServer(), so on the Java 11 legs every later restart (from this class's own @AfterClass on) logs ERR_TRUSTSTORESYNC_EXCEPTION for it, since Java 11 without BC-FIPS has no AES-KWP. Nothing fails, but deleting the entry in the finally would keep the shared instance free of a value only some runtimes can read.
  • 4. The RSASVE branch of decodeSymmetricKeyAttribute (CryptoManagerImpl.java:1037) fetches the instance certificate from the truststore a second time, right after getInstanceKeyID() at line 1007 fetched it. Fetching the bytes once and deriving both the key ID and the public key from them saves the extra internal search and guarantees the modulus is that of the certificate whose ID was just matched.
  • 5. testKnownAnswer (RsaSveKeyTransportTestCase.java:156) runs only with BC-FIPS doing the KWP. A JDK row (skipped on 11, like the other JDK rows) would pin the stored format for SunJCE too; it matches today, as noted above.
  • 6. TestCaseUtils.withBcFipsFirst (line 402) repeats the provider install/restore of the existing CryptoManagerTestCase.inApprovedOnlyBcFipsThread (lines 1297-1300, 1318-1325); the latter could call the former and keep only its thread and approved-only part.
  • 7. withoutKeyWrap (RsaSveKeyTransportTestCase.java:436, the RawRsaOfSunJce stand-in tied to com.sun.crypto.provider.RSACipher) is repeated in testRsaSveKeyWrappingWithoutAesKwpIsRefusedNamingTheProperty (CryptoManagerTestCase.java:943-953); moving it next to withoutJceService in TestCaseUtils would keep one copy.

@vharseko vharseko added the java Changes to Java sources label Oct 16, 2026
…vate key in an RSASVE unwrap

unwrap() still initialized the raw RSA cipher with the private key of
the instance inside the try which reports every failure as "cannot
unwrap the key". A provider which cannot use that key, such as a
SunPKCS11 runtime refusing the private key of the trust store, was
therefore reported like a forged value, and the real reason reached the
error log only as a cause, which the crypto manager's message drops.

The initialization does not depend on the value either, so it now comes
before the try, next to the lookup of the cipher: the provider's own
reason is reported, and what the private key recovers from a value
still shows in no message.

testUnwrapReportsWhyTheRsaCipherRefusesThePrivateKey unwraps with an EC
private key and expects the refusal of the RSA cipher itself. The
initialization comes before the lookup of AES-KWP, so the case runs on
Java 11 too.
…K 17 offers no AES-KWP

The FIPS install procedure targets a runtime whose cryptography comes
from SunPKCS11 alone and sets RSASVE/KDF-SHA256/AESKWP in its
changes.ldif, but named only Java 11 without Bouncy Castle FIPS as a
runtime without AES-KWP. The SunPKCS11 provider of OpenJDK 17, a common
FIPS runtime, has none either (no P11KeyWrapCipher in 17.0.7 and
17.0.11, present from 19.0.1 on), so an administrator following the
procedure there met a server which refuses the value at start.

The install guide and the description of key-wrapping-transformation
now say so, and from which release the provider offers it.
…en decoding a symmetric key

decodeSymmetricKeyAttribute() read the instance key certificate from
the trust store to compute the instance key ID, then, on the RSASVE
branch, read it a second time for the public key: one more internal
search per key, and nothing guaranteed that the modulus was that of the
certificate whose ID had just been matched.

The certificate is now read once; the key ID and the public key of the
RSASVE branch both come from those bytes.
…manager test generates

testRsaSveWrappedKeyIsPublishedAndUnwraps left its AES/CBC/PKCS5Padding
192-bit key, wrapped with RSASVE/KDF-SHA256/AESKWP, in cn=secret keys.
The admin data backend is not restored by restartServer(), so on Java 11
without Bouncy Castle FIPS, which has no AES-KWP, every later restart of
the instance logged ERR_TRUSTSTORESYNC_EXCEPTION for it.

The case now deletes the entries of that key once the configuration is
restored; what it checks afterwards decodes the value it read, not the
entry.
…rap of the Java runtime too

testKnownAnswer ran only with Bouncy Castle FIPS doing AES-KWP, so the
stored format was pinned for one key wrap implementation, while a
server without Bouncy Castle FIPS first wraps with that of the Java
runtime.

The case now runs once per provider set, like the round trips: the JDK
row is skipped on Java 11, which has no AES-KWP.
…ed-only thread through withBcFipsFirst

inApprovedOnlyBcFipsThread installed Bouncy Castle FIPS first and put
the providers back with its own copy of what TestCaseUtils.withBcFipsFirst
does. It now runs its thread inside withBcFipsFirst and keeps only the
thread and the approved-only mode.
…KWP fixture in TestCaseUtils

RsaSveKeyTransportTestCase.withoutKeyWrap and
testRsaSveKeyWrappingWithoutAesKwpIsRefusedNamingTheProperty each
withdrew AES/KWP/NoPadding with the same RawRsaOfSunJce stand-in for
the raw RSA of SunJCE. The fixture now lives in TestCaseUtils, next to
withoutJceService, and both use it.
@maximthomas

Copy link
Copy Markdown
Contributor Author

Pushed as 56e2452..9999dc5, one commit per point.

1. 56e2452: rsa.init(DECRYPT_MODE, privateKey) now runs before the try, right after Cipher.getInstance and before the AES-KWP lookup, so a provider which refuses the instance private key is reported with its own exception instead of cannot unwrap the key. testUnwrapReportsWhyTheRsaCipherRefusesThePrivateKey unwraps with an EC private key and expects the RSA cipher's own refusal (No installed provider supports this key: sun.security.ec.ECPrivateKeyImpl with the JDK providers). With init moved back into the try it fails with RSASVE/KDF-SHA256/AESKWP cannot unwrap the key (checked locally). Since the init comes before the KWP lookup, the case runs on Java 11 too.
One side effect: with delayed provider selection, Cipher.init rethrows a RuntimeException thrown by the first candidate provider, which the try used to wrap. In SunPKCS11 that is only ProviderException("Token has been removed") from Token.ensureValid(): P11RSACipher.implInit turns PKCS11Exception into InvalidKeyException. It now propagates the same way it already does from unwrapper.init on the RSA-OAEP branch of decodeSymmetricKeyAttribute.

2. ef5b6f4: the install guide paragraph and the property description now say that the SunPKCS11 provider of OpenJDK 17 has no AES-KWP, so the server refuses the value on an OpenJDK 17 runtime that has only that provider, and that the provider has AES-KWP from OpenJDK 19 on. Checked locally with jimage list: P11KeyWrapCipher is absent from Temurin 17.0.7 and 17.0.11, and present in 19.0.1, 21.0.2, 24.0.2, 25.0.2 and Zulu 26.

3. 38f8705: the case deletes the entries of its AES/CBC/PKCS5Padding-192 key in the finally that restores the configuration. The checks after it decode the value already read.

4. 3bcc552: decodeSymmetricKeyAttribute reads the certificate once and derives both the key ID and, on the RSASVE branch, the public key from those bytes.

5. fc8e87e: testKnownAnswer takes JDK and BC_FIPS rows like the round trips. The JDK row gives the same vector and is skipped on 11.

6. ae0e932: inApprovedOnlyBcFipsThread runs its thread inside withBcFipsFirst and keeps only the thread and the approved-only part.

7. 9999dc5: TestCaseUtils.withoutKeyWrap, next to withoutJceService, is used by both classes.

Local runs (tests/failures/errors/skipped):

Class JDK 26.0.1 JDK 11.0.25
RsaSveKeyTransportTestCase 45/0/0/0 45/0/0/10 (the JDK-provider rows and the 1024-bit case need AES/KWP/NoPadding, which JDK 11 lacks)
CryptoManagerTestCase 58/0/0/0 58/0/0/0
GetSymmetricKeyExtendedOperationTestCase 2/0/0/0 2/0/0/0

@maximthomas
maximthomas requested a review from vharseko October 16, 2026 09:13

@vharseko vharseko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks — all seven points are in; checked against 9999dc5:

  • 1. rsa.init now runs before the try (RsaSveKeyTransport.java:255), and testUnwrapReportsWhyTheRsaCipherRefusesThePrivateKey pins the provider's own message. The ProviderException side effect leaves the RSASVE branch exactly as exposed as unwrapper.init on the RSA-OAEP branch, which is fine with me.
  • 2. In, with one correction below.
  • 3–7. Done as described. inApprovedOnlyBcFipsThread keeps the same install/restore order through withBcFipsFirst, and the JDK row of testKnownAnswer gives the same vector.

Correction to 2: OpenJDK 18, not 19. The SunPKCS11 key wrap came with JDK-8264849 "Add KW and KWP support to PKCS11 provider", Fix Version 18 (release note JDK-8275675: "SunPKCS11 Provider Supports AES Cipher With KW and KWP Modes if Supported by PKCS11 Library"). The jdk-18-ga tag has P11KeyWrapCipher.java and registers AES/KWP/NoPadding at SunPKCS11.java:755. Your list goes from 17.0.11 to 19.0.1, so "from OpenJDK 19 on" in chap-install.adoc:849 and CryptoManagerConfiguration.xml:197 (and in the PR description) is one release late. 18 has long been out of support, so this only corrects the record. The registration also requires the PKCS#11 library to offer CKM_AES_KEY_WRAP_KWP, which the paragraph's general rule ("when it lacks one, the server refuses the value") already covers.

CodeQL is red on this push. Alert 1313 (java/rsa-without-oaep, line 254, the Cipher.getInstance of the unwrap) reopened when rsa.init moved. It is the same finding as 1310–1312, 1315 and 1316, which you dismissed as "won't fix", so it needs the same dismissal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs enhancement fips FIPS 140 mode: BC-FIPS provider, BCFKS keystores, approved algorithms java Changes to Java sources security Security fixes / CodeQL code-scanning alerts tests Test suites: fixing, enabling, un-disabling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crypto manager: a FIPS-approved key transport for JVMs whose only RSA cipher is PKCS#1 v1.5 (SunPKCS11)

3 participants