Ron Frederick ronf@timeheart.net writes:
When generating a new keypair, I think you’ll want the API to return both the private key and public key.
Agreed, and that's how key generation in Nettle works generally (with some exceptions, e.g., for ed25519 where private key is an arbitrary random octet string without any structure, there's only a function to compute corresponding public key).
In this case, what's a bit weird is that the private key includes a literal copy of the public key. And it's not so nice to force applications to allocate space for it twice.
When decrypting, you don’t need the public key. The decrypt call takes ciphertext and a private key and returns the original shared secret, assuming it was encrypted using the corresponding public_key.
Some other nettle functions using the private key takes the corresponding public key as an additional argument. E.g.,
void ed25519_sha512_sign (const uint8_t *pub, const uint8_t *priv, size_t length, const uint8_t *msg, uint8_t *signature);
I think that makes sense too, when the private key operation needs the public key to do its work.
- Would it be useful with an api where public and private keys can be
decoded from byte strings to an internal representation, to not have to redo the decoding on each operation? Or stick to only bytestring input and output?
I would stick to byte strings here. It’d be messy to be converting back and forth between the two representations, and you will need the byte string version to be able to send it over the network.
I agree that makes sense, at least for the first version.
- For private key decoding, it may happen that the private key is
invalid. The private key includes lots of mod 3 coefficients, where each coefficient is represented as two bits with valid values 00, 01, 10. What if caller passes 11? Hitting some undefined behaviour or an assertion failure for this isn't that nice. One could return an error (which would be somewhat natural if one has a private key decode function as above), or silently replace 11 values with 00? Neither the spec or the current code includes any error handling as far as I've found.
I haven’t looked at the internals, but if a caller passes in an invalid key (public or private), I would recommend returning an error and not trying to translate the key bytes into a “valid” key.
By the spec, the decoding functions applied to public keys and ciphertexts silently ignores errors. Concretely, when it decodes a couple of bits that are expected to represent a number r in the range 0 <= r < m, but in fact r >= m, it silently replaces r by r mod m.
I'm not sure this is great, but this part of the spec is pretty clear.
But no mention of how to deal with analogous errors when decoding a private key. Replacing bits 11 by 00 (reduce mod 3) would be somewhat consistent.
- I'm confused by the extra steps taken to get "implicit rejection".
Decryption may fail if fed a random (or maliciously chosen) ciphertext. It makes intuitive sense to me to hide all details of where during decryption something looked wrong. But why go to extra effort to not return any error at all? When the system is attacked, why does it matter if the attacker gets an explicit error at key exchange time, or an error (typically a MAC or AEAD decrypt failure) at first use of the resulting session key? I understand argument is likely subtle, but I'd appreciate any layman explanation for this design.
In the case of a bad key, the decrypt call probably wouldn’t be able to return any plaintext, so I think it pretty much has to return an error in this case to indicate that the plaintext buffer is not valid. We wouldn’t want the caller to try and access uninitialized memory.
There's no returned error. The definition of the private key includes an extra random value "rho", and in case decryption fails, the function returns a session key that is essentially the hash of rho and the input ciphertext. rho is not used for anything else. It's the value and purpose of this complication that has me a bit puzzled.
To summarize, I think the API should look something like:
keypair() -> (private_key, public_key) encaps(shared_secret, public_key) -> ciphertext
This is close to what the spec calls sntrup "core", but the advertised sntrup KEM doesn't take a shared secret as input, it generates a random input to the encapsulation procedure, and outputs a short session key that is the hash of that random input and other values. I think that's how other KEMs work too (and what makes the KEM interface different from traditional public key encryption)?
#define sntrup761_length_public_key 1158 #define sntrup761_length_private_key 1763
Private key size is another interesting detail.
As I read the spec, an sntrup761 private key consists of, in order:
* encodings of two polynomials with -1,0,1 coefficients (191 bytes each) * a copy of the public key, 1158 bytes (encoding a polynomial with larger coefficients) * the rho value mentioned above, another 191 bytes.
For a total of 1731 = 1158 + 3*191 bytes.
But then the current code appends another 32 bytes, which are just a hash of the public key. The only purpose seems to be to avoid a hash operation when the key is used, which seems like a rather questionable optimization to me. So we have a key size that is 1731 bytes according to spec, but 1763 in existing implementations?
Regards, /Niels