Repository navigation
[#1056] Add the RSASVE/KDF-SHA256/AESKWP key wrapping transformation for runtimes without RSA-OAEP - #1194
[#1056] Add the RSASVE/KDF-SHA256/AESKWP key wrapping transformation for runtimes without RSA-OAEP#1194maximthomas wants to merge 24 commits into
Conversation
…untimes without RSA-OAEP
…wrap to its message
…y wrapping transformation
… wrapping transformation
…heme as SP 800-56B section 9.3 to the letter
| final byte[] wrapped = new byte[buffer.remaining()]; | ||
| buffer.get(wrapped); | ||
|
|
||
| final Cipher rsa = Cipher.getInstance(RSA_TRANSFORMATION); |
…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.
vharseko
left a comment
There was a problem hiding this comment.
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, incn=secret keys. The admin backend isconfig/admin-backend.ldifand survivesrestartServer(), so on the Java 11 legs every later restart (from this class's own@AfterClasson) logsERR_TRUSTSTORESYNC_EXCEPTIONfor it, since Java 11 without BC-FIPS has no AES-KWP. Nothing fails, but deleting the entry in thefinallywould 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 aftergetInstanceKeyID()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 existingCryptoManagerTestCase.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, theRawRsaOfSunJcestand-in tied tocom.sun.crypto.provider.RSACipher) is repeated intestRsaSveKeyWrappingWithoutAesKwpIsRefusedNamingTheProperty(CryptoManagerTestCase.java:943-953); moving it next towithoutJceServiceinTestCaseUtilswould keep one copy.
…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.
|
Pushed as 56e2452..9999dc5, one commit per point. 1. 56e2452: 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 3. 38f8705: the case deletes the entries of its AES/CBC/PKCS5Padding-192 key in the 4. 3bcc552: 5. fc8e87e: 6. ae0e932: 7. 9999dc5: Local runs (tests/failures/errors/skipped):
|
vharseko
left a comment
There was a problem hiding this comment.
Thanks — all seven points are in; checked against 9999dc5:
- 1.
rsa.initnow runs before thetry(RsaSveKeyTransport.java:255), andtestUnwrapReportsWhyTheRsaCipherRefusesThePrivateKeypins the provider's own message. TheProviderExceptionside effect leaves the RSASVE branch exactly as exposed asunwrapper.initon the RSA-OAEP branch, which is fine with me. - 2. In, with one correction below.
- 3–7. Done as described.
inApprovedOnlyBcFipsThreadkeeps the same install/restore order throughwithBcFipsFirst, and the JDK row oftestKnownAnswergives 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.
Closes #1056.
Summary
A Java runtime whose cryptography comes from a
SunPKCS11provider alone, such asSunPKCS11-NSS-FIPSon 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 forkey-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 onlyRSA/ECB/NoPadding, SHA-256 and AES-KWP from the runtime. The scheme is opt-in only: the default staysRSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING, and neithersetupnor 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:AESWrapPadfirst, the only name Bouncy Castle FIPS knows, so that Bouncy Castle FIPS does the key wrap when it is installed first; thenAES/KWP/NoPadding. SunJCE and SunPKCS11 registerAESWrapPadas an alias of theirAES/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 existingds-cfg-symmetric-keyvaluewrappingKeyID: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:
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 implementRSAKey. 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 oneInvalidKeyException,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 asGeneralSecurityExceptionsubtypes, the only typeCryptoManagerImplcatches around the wrap and the unwrap.Crypto manager
CryptoManagerImplroutes to the new class by the transformation name, matched case-insensitively like a JCE transformation, in three places:isKeyWrappingTransformationSupportedchecks that the runtime offers the three algorithms. A missing one is refused through the existingERR_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.encodeSymmetricKeyAttributewraps with the scheme, passing the certificate so that its key usage is checked.decodeSymmetricKeyAttributeunwraps 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 reportERR_CRYPTOMGR_DECODE_SYMMETRIC_KEY_ATTRIBUTE_DECIPHER, whichCryptoManagerSynconly 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
key-wrapping-transformationinCryptoManagerConfiguration.xml, shown bydsconfighelp and the generated configuration reference.[#install-fips]inchap-install.adoc. It corrects what a SunPKCS11 runtime offers (RSA/ECB/PKCS1PaddingandRSA/ECB/NoPadding), describes the scheme, and uses it in thechanges.ldifexample.Both say that the
SunPKCS11provider 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 (P11KeyWrapCipheris 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):InvalidKeyException;AES/KWP/NoPadding.The round trips of the five keys run under both the JDK providers and Bouncy Castle FIPS (
AESWrapPadonly, and a raw RSA that drops leading zeros).CryptoManagerTestCase(+8 cases):key-wrapping-transformationandAES/KWP/NoPadding, on a runtime without AES-KWP;The cases which need AES-KWP run with Bouncy Castle FIPS first, so that JDK 11 has it.
TestCaseUtilsgets two fixtures:withBcFipsFirstinstalls Bouncy Castle FIPS first and puts the providers back afterwards (the existing approved-only fixture ofCryptoManagerTestCasenow uses it), andwithoutKeyWrapruns an action on a runtime without AES-KWP, as Java 11 without Bouncy Castle FIPS is.The
CryptoManagerTestCasecase 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:
RsaSveKeyTransportTestCaseAES/KWP/NoPadding, which JDK 11 lacks)CryptoManagerTestCaseGetSymmetricKeyExtendedOperationTestCaseResults 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-FIPSonly is still outstanding:The runtime's
SunPKCS11has to offerAES/KWP/NoPadding. In the upstream OpenJDK sources, 17.0.7 and 17.0.11 register no AES-KWP inSunPKCS11, 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.key-wrapping-transformationtoRSASVE/KDF-SHA256/AESKWPand start the server. This also exercises the wrap side through the trial wrap.ERR_TRUSTSTORESYNC_EXCEPTIONappears 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.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'sC_WrapKey.