Is there a KEM (Key Encapsulation Mechanism) architecture being proposed for the JCA?

David Hook dgh at cryptoworkshop.com
Sun Aug 21 03:57:31 UTC 2022


On rereading, I've realised I neglected to comment on the KeyAgreement 
class.

It might be useful to see how these things would look in practice. For 
the 3 code fragment below vPubKey and vPrivKey should be assumed to be 
the public and private keys of the recipient V. paramSpec is assumed to 
be carrying KDF or other details - it might be absent.

Using KeyAgreement looks like this:

     // U side

     KeyAgreement agreement = KeyAgreement.getInstance("Kyber");

     agreement.init(null, paramSpec);

     PublicKey uPub = agreement.doPhase(vPubKey, true);

     byte[] secret = agreement.generateSecret();

     // U sends uPub.getEncoded(), which is the encapsulation

     // V side - V has encoded a byte[]

     KeyFactory kFact = KeyFactory.getInstance("KyberEncapsulation");

     PublicKey uPub = kFact.generatePublic(new X509EncodedKeySpec(encoded));

     KeyAgreement agreement = KeyAgreement.getInstance("Kyber");

     agreement.init(vPrivKey, paramSpec);

     agreement.doPhase(uPub, true);  // return ignored

     byte[] secret = agreement.generateSecret();

Up side is there's no new API, down side is, to me at least, almost none 
of the above makes sense, why is null passed to U's init? why isn't uPub 
a real public key? why does the encapsulation have to be turned into a 
PublicKey? why is invalid data (probably) being passed to 
X509EncodedKeySpec? what possible relevance is boolean, other than it 
has to be there? Okay, I appreciate these things are in the eye of the 
beholder, but it just looks bad.

Here's what is currently done in BC:

         // U side
         KeyGenerator keyGen = KeyGenerator.getInstance("Kyber");

         keyGen.init(new KEMGenerateSpec(vPubKey, "AES"), new 
SecureRandom());

         SecretKeyWithEncapsulation secEnc1 = 
(SecretKeyWithEncapsulation)keyGen.generateKey();

         byte[] secret = secEnc1.getEncoded();

         // U sends secEnc1.getEncapsulation(), which is the encapsulation

         // V side - V has encoded a byte[]
         KeyGenerator keyGen = KeyGenerator.getInstance("Kyber");

         keyGen.init(new KEMExtractSpec(vPrivKey, encoded, "AES"));

         SecretKeyWithEncapsulation secEnc2 = 
(SecretKeyWithEncapsulation)keyGen.generateKey();

         byte[] secret = secEnc2.getEncoded();

This feels like it makes more sense, but it is stretching the 
KeyGenerator class a bit (suddenly some init() methods don't make sense) 
and we've also had to introduce "vendor specific" classes and it does 
mean you're always creating a SecretKey even if you just want to do a 
getEncoded() from it. As much as I prefer it, it still kind of sucks.

Here, I think, is what John was proposing earlier could look like:

         // U side
         KEM kem = KEM.getInstance("Kyber");

         kem.initGenerate(vPubKey, paramSpec, new SecureRandom());

         byte[] encapsulation = kem.generateEncapsulation();

         byte[] secret = kem.generateSecret();

         // U sends encapsulation to V

         // V side - V has encapsulation a byte[]
         KEM kem = KEM.getInstance("Kyber");

         kem.initExtract(vPrivKey, paramSpec);

         byte[] secret = kem.extractSecret(encapsulation);

As I said, "could look like", I'm not saying "should", but it's really 
much cleaner, simpler, and less confusing than the first two, and I 
really think the above demonstrates that these new algorithms are really 
not properly covered in the existing API.

Regards,

David
On 21/8/22 04:08, David Hook wrote:
> Hi Michael,
>
> I don't know anything about bureaucrats, I am an engineer. You may 
> need to consult someone else on bureaucrats.
>
> I apologize for my apparent deficiencies in this area, but would you 
> mind explaining how Cipher.wrap() is either supposed to take a public 
> key and create an encapsulation based on it and return a secret key 
> implicitly in one clean move, or why it even makes sense to do so. The 
> method was never conceived as providing the functionality for what a 
> KEM actually does, and when I did the initial PKCS11 implementation at 
> Eracom in the late 90's and the team at Sun added the wrap/unwrap 
> functions to support it, this is definitely not was intended either - 
> it was for explicit key wrapping based on the key that was passed to 
> Cipher.init().
>
> On BC's part, we've already implemented RFC 5990/SP 800-56B in Java 
> and the experience has, at best, been awkward. The new algorithms have 
> moved awkward to inappropriate. With the new algorithms, there's no 
> longer only one case of this, it's not an outlier, there should be a 
> general way of supporting KEMs in the API that doesn't involve over 
> engineering KeyGenerator and Cipher.
>
> I work with a team that has had to implement all of them and had to 
> make them fit into the JCA. We have done so. Like John, I am simply 
> relaying our experience. In about 18 months these algorithms are going 
> to become mandatory, what all of us think is irrelevant. We, for our 
> part, already have a solution, but we both realize it's not "the 
> solution" - we recognize that the JVM is uniquely positioned to 
> provide leadership on this and provide a universal way of doing it.
>
> Of course, if it's felt that these algorithms should be ignored, it's 
> not my place to revolt, although I do feel obliged to argue. I will 
> simply try and do the best by my users, as I have no doubt will John. 
> Both of us have simply offered our comments in good faith and to alert 
> the community that things have changed and that with these new 
> algorithms there is room for a new approach. The ambiguity about how 
> these algorithms can be implemented and the excessive need to fallback 
> on propritary classes for them does suggest that there are some 
> additions to the JCA which would help. I appreciate to understand this 
> statement does involve actually understanding what these algorithms do 
> and may require some additional reading.
>
> As I said, I'm an engineer, my users will be able to use these 
> algorithms properly, my team will ensure that, as I have no doubt will 
> John's. What John and myself, apparently mistakenly, care about is 
> that our users should also be able to use these algorithms portably.
>
> Are you saying portability is no longer a consideration?
>
> Regards,
>
> David
>
>
>
> On 21/8/22 02:23, Michael StJohns wrote:
>> Hi David/John -
>>
>> I would submit that you're trying too hard to make your life simple! :-)
>>
>> Cipher.wrap/unwrap are the correct methods.
>>
>> For example:
>>
>> Cipher  kem = Cipher.getInstance 
>> ("ECIES/GCM-128-64/KDF-SP800-108-COUNTER-SHA256");
>> kem.init (Cipher.WRAP_MODE, pubkey);
>> byte[] opaqueEncapsulatedKey = kem.wrap (someOtherKey);
>>
>> The "opaqueEncapsulatedKey" would contain the data needed by the 
>> unwrap function - specifically a) the ecies ephemeral public key, b) 
>> the fact that the derived key is a GCM key of length 128 and that the 
>> GCM tag is 64 bytes long, c) the KDF, d) (optional) any mixins other 
>> than defaults required by the KDF - which would be passed in a 
>> parameter blob during init.  Cipher would NOT return the underlying 
>> generated secret used to wrap the key.  Just the public part of the 
>> key pair used to do the ECDH operation against the passed in public 
>> key.   In the RSA case, the wrapped encrypting secret would be an 
>> opaque data blob and would be part of the data passed to the unwrap 
>> function.
>>
>> If you want a key generated for other purposes, then the right thing 
>> is using a KDF and a Key agreement function in tandem.   Strangely 
>> the KDF appears in the javacard API for 3.1, but not in the JCE/JDK API.
>>
>> "What's the difference between a bureaucrat and an engineer?  A 
>> bureaucrat takes small solvable pieces and combines them into one 
>> insoluble mass."
>>
>> In this case, Java provides a number of flexible primitives that can 
>> be combined as needed. In this case, the underlying Cipher 
>> implementation would wrap key agreement and kdf and cipher (GCM) 
>> instances.  It should return UnsupportedOperationException for all 
>> operations execept wrap/unwrap and the appropriate init methods.
>>
>> Later, Mike
>>
>>
>>
>> On 8/19/2022 6:38 PM, David Hook wrote:
>>> Hi Mike,
>>>
>>> KEMs can be used for key wrapping - we've actually implemented 
>>> support for this too. But they are not actually key wrapping ciphers.
>>>
>>> Here's a simple example of using Kyber for key wrapping in BC:
>>>
>>> SecretKey key =new SecretKeySpec(keyBytes,"AES");
>>>
>>> w1.init(Cipher.WRAP_MODE, kp.getPublic(),new KEMParameterSpec("AES-KWP"));
>>>
>>> byte[]data =w1.wrap(key);
>>>
>>> Cipher w2 =Cipher.getInstance(algorithm,"BCPQC");
>>>
>>> w2.init(Cipher.UNWRAP_MODE, kp.getPrivate(),new KEMParameterSpec("AES-KWP"));
>>>
>>> Key k =w2.unwrap(data,"AES",Cipher.SECRET_KEY);
>>>
>>> The behavior in this case is in line with what is given in RFC 5990 for the RSA KEM. How it works is by using the key generated
>>> by the KEM to create an AES-KWP key, which is then used to wrap keyBytes. The shortcoming is it means you have to generate the
>>> secret key separately.
>>>
>>> This is the problem though - a KEM can actually be used to generate a secret key for other purposes. For example, where
>>> someone is trying to implement a hybrid KAS scheme. But there is currently no mechanism in the Java APIs for being able to
>>> take advantage of this directly, hence our use of the KeyGenerator class and other people's attempts to make use of the KeyAgreement
>>> class. The Cipher.wrap() returns a byte[] - to be used with a KEM for secret generation it would also have to return the
>>> generated secret (I would probably also argue that passing a public key to wrap in order to generate an encapsulation of a
>>> generated encrypted secret was not the correct use of the API either, but the fact remains a byte[] is not really going to cut it).
>>>
>>> If you have any further questions, please feel free to ask. For what it is worth, I have been developing providers for the JCE/JCA since
>>> the late 90's and am actually one of the people responsible for the introduction of the existing wrap/unwrap API in the Cipher class.
>>>
>>> Thanks,
>>>
>>> David
>>> On 20/8/22 07:53, Mike StJohns wrote:
>>>> Hi This implemented as part of Javax.crypto.Cipher.  See the Java doc for the wrap and unwrap methods.
>>>>
>>>> Mike
>>>>
>>>> Sent from my iPad
>>>>
>>>>> On Aug 19, 2022, at 12:56, John Gray<John.Gray at entrust.com>  wrote:
>>>>>
>>>>>  We are starting to make use of the new PQ algorithms adopted by NIST for prototyping and development of standards.   In particular we are working on a composite KEM standard:
>>>>> See:https://datatracker.ietf.org/doc/draft-ounsworth-pq-composite-kem/
>>>>>
>>>>> However, there is no KEM interface in the JCA (which make sense because these are new algorithms, although RSA-KEM has been out since 2010).
>>>>>
>>>>> I can add one into our toolkit (and I think David may have already added on into BC),  but I assume at some point there will be an official one added in Java and likely it won't be identical to what we do even if it is very close, which would cause backwards compatibility pain...   Perhaps we could collaborate on extending the JCA to support KEM?      Essentially it requires methods.
>>>>>
>>>>> ss, ct := encapsulate(PublicKey)
>>>>> ss := decapsulate(PrivateKey, ct)
>>>>>
>>>>> -ss is a shared secret (could come back as a Java SecretKey if you wanted as it would usually be used to derive something like an AES afterwards)
>>>>> -ct is a Cipher Text (a byte array would make sense)
>>>>> -Public and Private Keys would use the regular public and private key interface.
>>>>> -An object holding the ss and ct from the encapsulate() method could be returned, with accessor methods to get the ss and ct.   It could be called 'EncapsulatedKEMData' for example.
>>>>>
>>>>> Likely you would want a new type of KEM crypto object (like you have for Signature, MessageDigest, Cipher, Mac, SecureRandom, KeyAgreement.. etc).   Calling it KEM would seem to make sense.    😊    It could also use similar calling patterns and have a KEM.initKEM(keypair.getPublic()) or KEM.initKEM(keypair.getPrivate()), and then you would just call KEM.encapsulate() or KEM.decapsulate(ct).
>>>>>
>>>>> Then algorithms could be registered in providers as usual:
>>>>>
>>>>>     put("KEM.Kyber","com.blah.Kyber")
>>>>>     put("KEM.compositeKEM","com.entrust.toolkit.crypto.kem.compositeKEM")
>>>>>
>>>>> Then the above methods (encapsulate and decapsulate) could be defined in that new object type.   Then we would be able to make use of it and not have to worry about incompatibility issues down the road...
>>>>>
>>>>> Cheers,
>>>>>
>>>>> John Gray
>>>>>
>>>>>
>>>>>
>>>>> Any email and files/attachments transmitted with it are confidential and are intended solely for the use of the individual or entity to whom they are addressed. If this message has been sent to you in error, you must not copy, distribute or disclose of the information it contains. Please notify Entrust immediately and delete the message from your system.
>>>
>>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/security-dev/attachments/20220821/e41dc5b7/attachment.htm>


More information about the security-dev mailing list