cloudflare / circl

CIRCL: Cloudflare Interoperable Reusable Cryptographic Library
http://blog.cloudflare.com/introducing-circl
Other
1.27k stars 142 forks source link

Unpacking Kyber keys from a FIPS 203 x509 key spec #496

Closed jmcrawford45 closed 5 months ago

jmcrawford45 commented 5 months ago

Hi all,

I'm encountering an issue where I'm able to correctly encapsulate / decapsulate with a randomly generated key, but I fail to decapsulate when unpacking from a FIPS 203 x509 key spec produced by BouncyCastle. I'm loading the public key from t + rho, and I'm loading the private key from s + t + rho + hpk + z. Does this sound like a reasonable mapping / are there some expected transformations required to agree on the same key?

Best, Jared

bwesterb commented 5 months ago

Did you use https://github.com/cloudflare/circl/pull/470 ?

jmcrawford45 commented 5 months ago

Yes, this is using https://github.com/cloudflare/circl/pull/470

bwesterb commented 5 months ago

Which version of bouncy castle? They did some weird things with the private key, and your version might still be on round 3 instead of the IPD.

jmcrawford45 commented 5 months ago

This is on latest release (1.78.1).

bwesterb commented 5 months ago

Ok, what is this "x509 key spec" about?

jmcrawford45 commented 5 months ago

The x509 aspect isn't relevant. Basically I'm passing the output of getEncoded from the key parameters into the relevant unpack functions. E.g. for a public key this is

static byte[] getEncoded(byte[] t, byte[] rho)
    {
        return Arrays.concatenate(t, rho);
    }

and for a private key this is

public byte[] getEncoded()
    {
        return Arrays.concatenate(new byte[][]{ s, t, rho, hpk, nonce });
    }

Here's my relevant go code

    pqPubKeyBytes, err := base64.StdEncoding.DecodeString("...")
    if err != nil {
        return nil, err
    }
    pqPubKey := mlkem768.PublicKey{}
    pqPubKey.Unpack(pqPubKeyBytes)
    if err != nil {
        return nil, err
    }
    pqPrivKeyBytes, err := base64.StdEncoding.DecodeString(...)

    pqPrivKey := mlkem768.PrivateKey{}
    pqPrivKey.Unpack(pqPrivKeyBytes)
bwesterb commented 5 months ago

With any sane implementation of Kyber, the user should never have to handle the constituents (t, s, etc) of the keys themselves. What is that about?

jmcrawford45 commented 5 months ago

I'm have two use cases for loading an existing key generated by BouncyCastle. One is for known answer tests to verify compatibility, and the other is the more standard use case of key encapsulation which requires loading the public key. I'm not directly accessing the constituents, I'm using the getEncoded method and the unpack methods which from reading the code seem to agree on the arrangement and format of constituents. Is there another preferred way to load an existing mlkem key pair in circl?

bwesterb commented 5 months ago

ML-KEM public and private keys are opaque byte strings. You shouldn't have to deal with the constituents. We pass the test vectors from the reference implementation (round 3, ipd), which checks the private and public key format. It's likely something is wrong on the BouncyCastle side of things. Happy to help debug, but it's hard from a distance.

jmcrawford45 commented 5 months ago

This was an issue with BouncyCastle version conflicts. Thanks again for taking a look!