diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index edb02ce4..0266b2d1 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -20,6 +20,7 @@ add_sources_from_current_dir(crypto ecc-ssh.c hash_simple.c hmac.c + kex-hybrid.c mac.c mac_simple.c md5.c diff --git a/crypto/kex-hybrid.c b/crypto/kex-hybrid.c new file mode 100644 index 00000000..38390bd0 --- /dev/null +++ b/crypto/kex-hybrid.c @@ -0,0 +1,322 @@ +/* + * Centralised machinery for hybridised post-quantum + classical key + * exchange setups, using the same message structure as ECDH but the + * strings sent each way are the concatenation of a key or ciphertext + * of each type, and the output shared secret is obtained by hashing + * together both of the sub-methods' outputs. + */ + +#include +#include +#include + +#include "putty.h" +#include "ssh.h" +#include "mpint.h" + +/* ---------------------------------------------------------------------- + * Common definitions between client and server sides. + */ + +typedef struct hybrid_alg hybrid_alg; + +struct hybrid_alg { + const ssh_hashalg *combining_hash; + const pq_kemalg *pq_alg; + const ssh_kex *classical_alg; + void (*reformat)(ptrlen input, BinarySink *output); +}; + +static char *hybrid_description(const ssh_kex *kex) +{ + const struct hybrid_alg *alg = kex->extra; + + /* Bit of a bodge, but think up a short name to describe the + * classical algorithm */ + const char *classical_name; + if (alg->classical_alg == &ssh_ec_kex_curve25519) + classical_name = "Curve25519"; + else + unreachable("don't have a name for this classical alg"); + + return dupprintf("%s / %s hybrid key exchange", + alg->pq_alg->description, classical_name); +} + +static void reformat_mpint_be(ptrlen input, BinarySink *output, size_t bytes) +{ + BinarySource src[1]; + BinarySource_BARE_INIT_PL(src, input); + mp_int *mp = get_mp_ssh2(src); + assert(!get_err(src)); + assert(get_avail(src) == 0); + for (size_t i = bytes; i-- > 0 ;) + put_byte(output, mp_get_byte(mp, i)); + mp_free(mp); +} + +static void reformat_mpint_be_32(ptrlen input, BinarySink *output) +{ + reformat_mpint_be(input, output, 32); +} + +static void reformat_mpint_be_48(ptrlen input, BinarySink *output) +{ + reformat_mpint_be(input, output, 48); +} + +/* ---------------------------------------------------------------------- + * Client side. + */ + +typedef struct hybrid_client_state hybrid_client_state; + +static const ecdh_keyalg hybrid_client_vt; + +struct hybrid_client_state { + const hybrid_alg *alg; + strbuf *pq_ek; + pq_kem_dk *pq_dk; + ecdh_key *classical; + ecdh_key ek; +}; + +static ecdh_key *hybrid_client_new(const ssh_kex *kex, bool is_server) +{ + assert(!is_server); + hybrid_client_state *s = snew(hybrid_client_state); + s->alg = kex->extra; + s->ek.vt = &hybrid_client_vt; + s->pq_ek = strbuf_new(); + s->pq_dk = pq_kem_keygen(s->alg->pq_alg, BinarySink_UPCAST(s->pq_ek)); + s->classical = ecdh_key_new(s->alg->classical_alg, is_server); + return &s->ek; +} + +static void hybrid_client_free(ecdh_key *ek) +{ + hybrid_client_state *s = container_of(ek, hybrid_client_state, ek); + strbuf_free(s->pq_ek); + pq_kem_free_dk(s->pq_dk); + ecdh_key_free(s->classical); + sfree(s); +} + +/* + * In the client, getpublic is called first: we make up a KEM key + * pair, and send the public key along with a classical DH value. + */ +static void hybrid_client_getpublic(ecdh_key *ek, BinarySink *bs) +{ + hybrid_client_state *s = container_of(ek, hybrid_client_state, ek); + put_datapl(bs, ptrlen_from_strbuf(s->pq_ek)); + ecdh_key_getpublic(s->classical, bs); +} + +/* + * In the client, getkey is called second, after the server sends its + * response: we use our KEM private key to decapsulate the server's + * ciphertext. + */ +static bool hybrid_client_getkey(ecdh_key *ek, ptrlen remoteKey, BinarySink *bs) +{ + hybrid_client_state *s = container_of(ek, hybrid_client_state, ek); + + BinarySource src[1]; + BinarySource_BARE_INIT_PL(src, remoteKey); + + ssh_hash *h = ssh_hash_new(s->alg->combining_hash); + + ptrlen pq_ciphertext = get_data(src, s->alg->pq_alg->c_len); + if (get_err(src)) { + ssh_hash_free(h); + return false; /* not enough data */ + } + if (!pq_kem_decaps(s->pq_dk, BinarySink_UPCAST(h), pq_ciphertext)) { + ssh_hash_free(h); + return false; /* pq ciphertext didn't validate */ + } + + ptrlen classical_data = get_data(src, get_avail(src)); + strbuf *classical_key = strbuf_new(); + if (!ecdh_key_getkey(s->classical, classical_data, + BinarySink_UPCAST(classical_key))) { + ssh_hash_free(h); + return false; /* classical DH key didn't validate */ + } + s->alg->reformat(ptrlen_from_strbuf(classical_key), BinarySink_UPCAST(h)); + strbuf_free(classical_key); + + /* + * Finish up: compute the final output hash and return it encoded + * as a string. + */ + unsigned char hashdata[MAX_HASH_LEN]; + ssh_hash_final(h, hashdata); + put_stringpl(bs, make_ptrlen(hashdata, s->alg->combining_hash->hlen)); + smemclr(hashdata, sizeof(hashdata)); + + return true; +} + +static const ecdh_keyalg hybrid_client_vt = { + .new = hybrid_client_new, /* but normally the selector calls this */ + .free = hybrid_client_free, + .getpublic = hybrid_client_getpublic, + .getkey = hybrid_client_getkey, + .description = hybrid_description, +}; + +/* ---------------------------------------------------------------------- + * Server side. + */ + +typedef struct hybrid_server_state hybrid_server_state; + +static const ecdh_keyalg hybrid_server_vt; + +struct hybrid_server_state { + const hybrid_alg *alg; + strbuf *pq_ciphertext; + ecdh_key *classical; + ecdh_key ek; +}; + +static ecdh_key *hybrid_server_new(const ssh_kex *kex, bool is_server) +{ + assert(is_server); + hybrid_server_state *s = snew(hybrid_server_state); + s->alg = kex->extra; + s->ek.vt = &hybrid_server_vt; + s->pq_ciphertext = strbuf_new_nm(); + s->classical = ecdh_key_new(s->alg->classical_alg, is_server); + return &s->ek; +} + +static void hybrid_server_free(ecdh_key *ek) +{ + hybrid_server_state *s = container_of(ek, hybrid_server_state, ek); + strbuf_free(s->pq_ciphertext); + ecdh_key_free(s->classical); + sfree(s); +} + +/* + * In the server, getkey is called first: we receive a KEM encryption + * key from the client and encapsulate a secret with it. We write the + * output secret to bs; the data we'll send to the client is saved to + * return from getpublic. + */ +static bool hybrid_server_getkey(ecdh_key *ek, ptrlen remoteKey, BinarySink *bs) +{ + hybrid_server_state *s = container_of(ek, hybrid_server_state, ek); + + BinarySource src[1]; + BinarySource_BARE_INIT_PL(src, remoteKey); + + ssh_hash *h = ssh_hash_new(s->alg->combining_hash); + + ptrlen pq_ek = get_data(src, s->alg->pq_alg->ek_len); + if (get_err(src)) { + ssh_hash_free(h); + return false; /* not enough data */ + } + if (!pq_kem_encaps(s->alg->pq_alg, + BinarySink_UPCAST(s->pq_ciphertext), + BinarySink_UPCAST(h), pq_ek)) { + ssh_hash_free(h); + return false; /* pq encryption key didn't validate */ + } + + ptrlen classical_data = get_data(src, get_avail(src)); + strbuf *classical_key = strbuf_new(); + if (!ecdh_key_getkey(s->classical, classical_data, + BinarySink_UPCAST(classical_key))) { + ssh_hash_free(h); + return false; /* classical DH key didn't validate */ + } + s->alg->reformat(ptrlen_from_strbuf(classical_key), BinarySink_UPCAST(h)); + strbuf_free(classical_key); + + /* + * Finish up: compute the final output hash and return it encoded + * as a string. + */ + unsigned char hashdata[MAX_HASH_LEN]; + ssh_hash_final(h, hashdata); + put_stringpl(bs, make_ptrlen(hashdata, s->alg->combining_hash->hlen)); + smemclr(hashdata, sizeof(hashdata)); + + return true; +} + +static void hybrid_server_getpublic(ecdh_key *ek, BinarySink *bs) +{ + hybrid_server_state *s = container_of(ek, hybrid_server_state, ek); + put_datapl(bs, ptrlen_from_strbuf(s->pq_ciphertext)); + ecdh_key_getpublic(s->classical, bs); +} + +static const ecdh_keyalg hybrid_server_vt = { + .new = hybrid_server_new, /* but normally the selector calls this */ + .free = hybrid_server_free, + .getkey = hybrid_server_getkey, + .getpublic = hybrid_server_getpublic, + .description = hybrid_description, +}; + +/* ---------------------------------------------------------------------- + * Selector vtable that instantiates the appropriate one of the above, + * depending on is_server. + */ + +static ecdh_key *hybrid_selector_new(const ssh_kex *kex, bool is_server) +{ + if (is_server) + return hybrid_server_new(kex, is_server); + else + return hybrid_client_new(kex, is_server); +} + +static const ecdh_keyalg hybrid_selector_vt = { + /* This is a never-instantiated vtable which only implements the + * functions that don't require an instance. */ + .new = hybrid_selector_new, + .description = hybrid_description, +}; + +/* ---------------------------------------------------------------------- + * Actual KEX methods. + */ + +static const hybrid_alg ssh_ntru_curve25519_hybrid = { + .combining_hash = &ssh_sha512, + .pq_alg = &ssh_ntru, + .classical_alg = &ssh_ec_kex_curve25519, + .reformat = reformat_mpint_be_32, +}; + +static const ssh_kex ssh_ntru_curve25519 = { + .name = "sntrup761x25519-sha512", + .main_type = KEXTYPE_ECDH, + .hash = &ssh_sha512, + .ecdh_vt = &hybrid_selector_vt, + .extra = &ssh_ntru_curve25519_hybrid, +}; + +static const ssh_kex ssh_ntru_curve25519_openssh = { + .name = "sntrup761x25519-sha512@openssh.com", + .main_type = KEXTYPE_ECDH, + .hash = &ssh_sha512, + .ecdh_vt = &hybrid_selector_vt, + .extra = &ssh_ntru_curve25519_hybrid, +}; + +static const ssh_kex *const ntru_hybrid_list[] = { + &ssh_ntru_curve25519, + &ssh_ntru_curve25519_openssh, +}; + +const ssh_kexes ssh_ntru_hybrid_kex = { + lenof(ntru_hybrid_list), ntru_hybrid_list, +}; diff --git a/crypto/ntru.c b/crypto/ntru.c index a7e53122..80c35179 100644 --- a/crypto/ntru.c +++ b/crypto/ntru.c @@ -1432,14 +1432,7 @@ static void ntru_session_hash( } /* ---------------------------------------------------------------------- - * Top-level key exchange and SSH integration. - * - * Although this system borrows the ECDH packet structure, it's unlike - * true ECDH in that it is completely asymmetric between client and - * server. So we have two separate vtables of methods for the two - * sides of the system, and a third vtable containing only the class - * methods, in particular a constructor which chooses which one to - * instantiate. + * Top-level KEM functions. */ /* @@ -1451,257 +1444,30 @@ static void ntru_session_hash( #define q_LIVE 4591 #define w_LIVE 286 -static char *ssh_ntru_description(const ssh_kex *kex) -{ - return dupprintf("NTRU Prime / Curve25519 hybrid key exchange"); -} - -/* - * State structure for the client, which takes the role of inventing a - * key pair and decrypting a secret plaintext sent to it by the server. - */ -typedef struct ntru_client_key { +struct ntru_dk { NTRUKeyPair *keypair; - ecdh_key *curve25519; - - ecdh_key ek; -} ntru_client_key; - -static void ssh_ntru_client_free(ecdh_key *dh); -static void ssh_ntru_client_getpublic(ecdh_key *dh, BinarySink *bs); -static bool ssh_ntru_client_getkey(ecdh_key *dh, ptrlen remoteKey, - BinarySink *bs); - -static const ecdh_keyalg ssh_ntru_client_vt = { - /* This vtable has no 'new' method, because it's constructed via - * the selector vt below */ - .free = ssh_ntru_client_free, - .getpublic = ssh_ntru_client_getpublic, - .getkey = ssh_ntru_client_getkey, - .description = ssh_ntru_description, + strbuf *encoded; + pq_kem_dk dk; }; -static ecdh_key *ssh_ntru_client_new(void) +static pq_kem_dk *ntru_vt_keygen(const pq_kemalg *alg, BinarySink *ek) { - ntru_client_key *nk = snew(ntru_client_key); - nk->ek.vt = &ssh_ntru_client_vt; - - nk->keypair = ntru_keygen(p_LIVE, q_LIVE, w_LIVE); - nk->curve25519 = ecdh_key_new(&ssh_ec_kex_curve25519, false); - - return &nk->ek; + struct ntru_dk *ndk = snew(struct ntru_dk); + ndk->dk.vt = alg; + ndk->encoded = strbuf_new_nm(); + ndk->keypair = ntru_keygen(p_LIVE, q_LIVE, w_LIVE); + ntru_encode_pubkey(ndk->keypair->h, p_LIVE, q_LIVE, ek); + return &ndk->dk; } -static void ssh_ntru_client_free(ecdh_key *dh) +static bool ntru_vt_encaps(const pq_kemalg *alg, BinarySink *c, BinarySink *k, + ptrlen ek) { - ntru_client_key *nk = container_of(dh, ntru_client_key, ek); - ntru_keypair_free(nk->keypair); - ecdh_key_free(nk->curve25519); - sfree(nk); -} - -static void ssh_ntru_client_getpublic(ecdh_key *dh, BinarySink *bs) -{ - ntru_client_key *nk = container_of(dh, ntru_client_key, ek); - - /* - * The client's public information is a single SSH string - * containing the NTRU public key and the Curve25519 public point - * concatenated. So write both of those into the output - * BinarySink. - */ - ntru_encode_pubkey(nk->keypair->h, p_LIVE, q_LIVE, bs); - ecdh_key_getpublic(nk->curve25519, bs); -} - -static bool ssh_ntru_client_getkey(ecdh_key *dh, ptrlen remoteKey, - BinarySink *bs) -{ - ntru_client_key *nk = container_of(dh, ntru_client_key, ek); - - /* - * We expect the server to have sent us a string containing a - * ciphertext, a confirmation hash, and a Curve25519 public point. - * Extract all three. - */ BinarySource src[1]; - BinarySource_BARE_INIT_PL(src, remoteKey); - - uint16_t *ciphertext = snewn(p_LIVE, uint16_t); - ptrlen ciphertext_encoded = ntru_decode_ciphertext( - ciphertext, nk->keypair, src); - ptrlen confirmation_hash = get_data(src, 32); - ptrlen curve25519_remoteKey = get_data(src, 32); - - if (get_err(src) || get_avail(src)) { - /* Hard-fail if the input wasn't exactly the right length */ - ring_free(ciphertext, p_LIVE); - return false; - } - - /* - * Main hash object which will combine the NTRU and Curve25519 - * outputs. - */ - ssh_hash *h = ssh_hash_new(&ssh_sha512); - - /* Reusable buffer for storing various hash outputs. */ - uint8_t hashdata[64]; - - /* - * NTRU side. - */ - { - /* Decrypt the ciphertext to recover the server's plaintext */ - uint16_t *plaintext = snewn(p_LIVE, uint16_t); - ntru_decrypt(plaintext, ciphertext, nk->keypair); - - /* Make the confirmation hash */ - ntru_confirmation_hash(hashdata, plaintext, nk->keypair->h, - p_LIVE, q_LIVE); - - /* Check it matches the one the server sent */ - unsigned ok = smemeq(hashdata, confirmation_hash.ptr, 32); - - /* If not, substitute in rho for the plaintext in the session hash */ - unsigned mask = ok-1; - for (size_t i = 0; i < p_LIVE; i++) - plaintext[i] ^= mask & (plaintext[i] ^ nk->keypair->rho[i]); - - /* Compute the session hash, whether or not we did that */ - ntru_session_hash(hashdata, ok, plaintext, p_LIVE, ciphertext_encoded, - confirmation_hash); - - /* Free temporary values */ - ring_free(plaintext, p_LIVE); - ring_free(ciphertext, p_LIVE); - - /* And put the NTRU session hash into the main hash object. */ - put_data(h, hashdata, 32); - } - - /* - * Curve25519 side. - */ - { - strbuf *otherkey = strbuf_new_nm(); - - /* Call out to Curve25519 to compute the shared secret from that - * kex method */ - bool ok = ecdh_key_getkey(nk->curve25519, curve25519_remoteKey, - BinarySink_UPCAST(otherkey)); - - /* If that failed (which only happens if the other end does - * something wrong, like sending a low-order curve point - * outside the subgroup it's supposed to), we might as well - * just abort and return failure. That's what we'd have done - * in standalone Curve25519. */ - if (!ok) { - ssh_hash_free(h); - smemclr(hashdata, sizeof(hashdata)); - strbuf_free(otherkey); - return false; - } - - /* - * ecdh_key_getkey will have returned us a chunk of data - * containing an encoded mpint, which is how the Curve25519 - * output normally goes into the exchange hash. But in this - * context we want to treat it as a fixed big-endian 32 bytes, - * so extract it from its encoding and put it into the main - * hash object in the new format. - */ - BinarySource src[1]; - BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(otherkey)); - mp_int *curvekey = get_mp_ssh2(src); - - for (unsigned i = 32; i-- > 0 ;) - put_byte(h, mp_get_byte(curvekey, i)); - - mp_free(curvekey); - strbuf_free(otherkey); - } - - /* - * Finish up: compute the final output hash (full 64 bytes of - * SHA-512 this time), and return it encoded as a string. - */ - ssh_hash_final(h, hashdata); - put_stringpl(bs, make_ptrlen(hashdata, sizeof(hashdata))); - smemclr(hashdata, sizeof(hashdata)); - - return true; -} - -/* - * State structure for the server, which takes the role of inventing a - * secret plaintext and sending it to the client encrypted with the - * public key the client sent. - */ -typedef struct ntru_server_key { - uint16_t *plaintext; - strbuf *ciphertext_encoded, *confirmation_hash; - ecdh_key *curve25519; - - ecdh_key ek; -} ntru_server_key; - -static void ssh_ntru_server_free(ecdh_key *dh); -static void ssh_ntru_server_getpublic(ecdh_key *dh, BinarySink *bs); -static bool ssh_ntru_server_getkey(ecdh_key *dh, ptrlen remoteKey, - BinarySink *bs); - -static const ecdh_keyalg ssh_ntru_server_vt = { - /* This vtable has no 'new' method, because it's constructed via - * the selector vt below */ - .free = ssh_ntru_server_free, - .getpublic = ssh_ntru_server_getpublic, - .getkey = ssh_ntru_server_getkey, - .description = ssh_ntru_description, -}; - -static ecdh_key *ssh_ntru_server_new(void) -{ - ntru_server_key *nk = snew(ntru_server_key); - nk->ek.vt = &ssh_ntru_server_vt; - - nk->plaintext = snewn(p_LIVE, uint16_t); - nk->ciphertext_encoded = strbuf_new_nm(); - nk->confirmation_hash = strbuf_new_nm(); - ntru_gen_short(nk->plaintext, p_LIVE, w_LIVE); - - nk->curve25519 = ecdh_key_new(&ssh_ec_kex_curve25519, false); - - return &nk->ek; -} - -static void ssh_ntru_server_free(ecdh_key *dh) -{ - ntru_server_key *nk = container_of(dh, ntru_server_key, ek); - ring_free(nk->plaintext, p_LIVE); - strbuf_free(nk->ciphertext_encoded); - strbuf_free(nk->confirmation_hash); - ecdh_key_free(nk->curve25519); - sfree(nk); -} - -static bool ssh_ntru_server_getkey(ecdh_key *dh, ptrlen remoteKey, - BinarySink *bs) -{ - ntru_server_key *nk = container_of(dh, ntru_server_key, ek); - - /* - * In the server, getkey is called first, with the public - * information received from the client. We expect the client to - * have sent us a string containing a public key and a Curve25519 - * public point. - */ - BinarySource src[1]; - BinarySource_BARE_INIT_PL(src, remoteKey); + BinarySource_BARE_INIT_PL(src, ek); uint16_t *pubkey = snewn(p_LIVE, uint16_t); ntru_decode_pubkey(pubkey, p_LIVE, q_LIVE, src); - ptrlen curve25519_remoteKey = get_data(src, 32); if (get_err(src) || get_avail(src)) { /* Hard-fail if the input wasn't exactly the right length */ @@ -1709,141 +1475,107 @@ static bool ssh_ntru_server_getkey(ecdh_key *dh, ptrlen remoteKey, return false; } - /* - * Main hash object which will combine the NTRU and Curve25519 - * outputs. - */ - ssh_hash *h = ssh_hash_new(&ssh_sha512); + /* Invent a valid NTRU plaintext. */ + uint16_t *plaintext = snewn(p_LIVE, uint16_t); + ntru_gen_short(plaintext, p_LIVE, w_LIVE); - /* Reusable buffer for storing various hash outputs. */ - uint8_t hashdata[64]; + /* Encrypt the plaintext, and encode the ciphertext into a strbuf, + * so we can reuse it for both the session hash and sending to the + * client. */ + uint16_t *ciphertext = snewn(p_LIVE, uint16_t); + ntru_encrypt(ciphertext, plaintext, pubkey, p_LIVE, q_LIVE); + strbuf *ciphertext_encoded = strbuf_new_nm(); + ntru_encode_ciphertext(ciphertext, p_LIVE, q_LIVE, + BinarySink_UPCAST(ciphertext_encoded)); + put_datapl(c, ptrlen_from_strbuf(ciphertext_encoded)); - /* - * NTRU side. - */ - { - /* Encrypt the plaintext we generated at construction time, - * and encode the ciphertext into a strbuf so we can reuse it - * for both the session hash and sending to the client. */ - uint16_t *ciphertext = snewn(p_LIVE, uint16_t); - ntru_encrypt(ciphertext, nk->plaintext, pubkey, p_LIVE, q_LIVE); - ntru_encode_ciphertext(ciphertext, p_LIVE, q_LIVE, - BinarySink_UPCAST(nk->ciphertext_encoded)); - ring_free(ciphertext, p_LIVE); + /* Compute the confirmation hash, and append that to the data sent + * to the other side. */ + uint8_t confhash[32]; + ntru_confirmation_hash(confhash, plaintext, pubkey, p_LIVE, q_LIVE); + put_data(c, confhash, 32); - /* Compute the confirmation hash, and write it into another - * strbuf. */ - ntru_confirmation_hash(hashdata, nk->plaintext, pubkey, - p_LIVE, q_LIVE); - put_data(nk->confirmation_hash, hashdata, 32); + /* Compute the session hash, i.e. the output shared secret. */ + uint8_t sesshash[32]; + ntru_session_hash(sesshash, 1, plaintext, p_LIVE, + ptrlen_from_strbuf(ciphertext_encoded), + make_ptrlen(confhash, 32)); + put_data(k, sesshash, 32); - /* Compute the session hash (which is easy on the server side, - * requiring no conditional substitution). */ - ntru_session_hash(hashdata, 1, nk->plaintext, p_LIVE, - ptrlen_from_strbuf(nk->ciphertext_encoded), - ptrlen_from_strbuf(nk->confirmation_hash)); - - /* And put the NTRU session hash into the main hash object. */ - put_data(h, hashdata, 32); - - /* Now we can free the public key */ - ring_free(pubkey, p_LIVE); - } - - /* - * Curve25519 side. - */ - { - strbuf *otherkey = strbuf_new_nm(); - - /* Call out to Curve25519 to compute the shared secret from that - * kex method */ - bool ok = ecdh_key_getkey(nk->curve25519, curve25519_remoteKey, - BinarySink_UPCAST(otherkey)); - /* As on the client side, abort if Curve25519 reported failure */ - if (!ok) { - ssh_hash_free(h); - smemclr(hashdata, sizeof(hashdata)); - strbuf_free(otherkey); - return false; - } - - /* As on the client side, decode Curve25519's mpint so we can - * re-encode it appropriately for our hash preimage */ - BinarySource src[1]; - BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(otherkey)); - mp_int *curvekey = get_mp_ssh2(src); - - for (unsigned i = 32; i-- > 0 ;) - put_byte(h, mp_get_byte(curvekey, i)); - - mp_free(curvekey); - strbuf_free(otherkey); - } - - /* - * Finish up: compute the final output hash (full 64 bytes of - * SHA-512 this time), and return it encoded as a string. - */ - ssh_hash_final(h, hashdata); - put_stringpl(bs, make_ptrlen(hashdata, sizeof(hashdata))); - smemclr(hashdata, sizeof(hashdata)); + ring_free(pubkey, p_LIVE); + ring_free(plaintext, p_LIVE); + ring_free(ciphertext, p_LIVE); + strbuf_free(ciphertext_encoded); + smemclr(confhash, sizeof(confhash)); + smemclr(sesshash, sizeof(sesshash)); return true; } -static void ssh_ntru_server_getpublic(ecdh_key *dh, BinarySink *bs) +static bool ntru_vt_decaps(pq_kem_dk *dk, BinarySink *k, ptrlen c) { - ntru_server_key *nk = container_of(dh, ntru_server_key, ek); + struct ntru_dk *ndk = container_of(dk, struct ntru_dk, dk); - /* - * In the server, this function is called after getkey, so we - * already have all our pieces prepared. Just concatenate them all - * into the 'server's public data' string to go in ECDH_REPLY. - */ - put_datapl(bs, ptrlen_from_strbuf(nk->ciphertext_encoded)); - put_datapl(bs, ptrlen_from_strbuf(nk->confirmation_hash)); - ecdh_key_getpublic(nk->curve25519, bs); + /* Expect a string containing a ciphertext and a confirmation hash. */ + BinarySource src[1]; + BinarySource_BARE_INIT_PL(src, c); + + uint16_t *ciphertext = snewn(p_LIVE, uint16_t); + ptrlen ciphertext_encoded = ntru_decode_ciphertext( + ciphertext, ndk->keypair, src); + ptrlen confirmation_hash = get_data(src, 32); + + if (get_err(src) || get_avail(src)) { + /* Hard-fail if the input wasn't exactly the right length */ + ring_free(ciphertext, p_LIVE); + return false; + } + + /* Decrypt the ciphertext to recover the sender's plaintext */ + uint16_t *plaintext = snewn(p_LIVE, uint16_t); + ntru_decrypt(plaintext, ciphertext, ndk->keypair); + + /* Make the confirmation hash */ + uint8_t confhash[32]; + ntru_confirmation_hash(confhash, plaintext, ndk->keypair->h, + p_LIVE, q_LIVE); + + /* Check it matches the one the server sent */ + unsigned ok = smemeq(confhash, confirmation_hash.ptr, 32); + + /* If not, substitute in rho for the plaintext in the session hash */ + unsigned mask = ok-1; + for (size_t i = 0; i < p_LIVE; i++) + plaintext[i] ^= mask & (plaintext[i] ^ ndk->keypair->rho[i]); + + /* Compute the session hash, whether or not we did that */ + uint8_t sesshash[32]; + ntru_session_hash(sesshash, ok, plaintext, p_LIVE, ciphertext_encoded, + confirmation_hash); + put_data(k, sesshash, 32); + + ring_free(plaintext, p_LIVE); + ring_free(ciphertext, p_LIVE); + smemclr(confhash, sizeof(confhash)); + smemclr(sesshash, sizeof(sesshash)); + + return true; } -/* ---------------------------------------------------------------------- - * Selector vtable that instantiates the appropriate one of the above, - * depending on is_server. - */ -static ecdh_key *ssh_ntru_new(const ssh_kex *kex, bool is_server) +static void ntru_vt_free_dk(pq_kem_dk *dk) { - if (is_server) - return ssh_ntru_server_new(); - else - return ssh_ntru_client_new(); + struct ntru_dk *ndk = container_of(dk, struct ntru_dk, dk); + strbuf_free(ndk->encoded); + ntru_keypair_free(ndk->keypair); + sfree(ndk); } -static const ecdh_keyalg ssh_ntru_selector_vt = { - /* This is a never-instantiated vtable which only implements the - * functions that don't require an instance. */ - .new = ssh_ntru_new, - .description = ssh_ntru_description, +const pq_kemalg ssh_ntru = { + .keygen = ntru_vt_keygen, + .encaps = ntru_vt_encaps, + .decaps = ntru_vt_decaps, + .free_dk = ntru_vt_free_dk, + .description = "NTRU Prime", + .ek_len = 1158, + .c_len = 1039, }; - -static const ssh_kex ssh_ntru_curve25519_openssh = { - .name = "sntrup761x25519-sha512@openssh.com", - .main_type = KEXTYPE_ECDH, - .hash = &ssh_sha512, - .ecdh_vt = &ssh_ntru_selector_vt, -}; - -static const ssh_kex ssh_ntru_curve25519 = { - /* Same as sntrup761x25519-sha512@openssh.com but with an - * IANA-assigned name */ - .name = "sntrup761x25519-sha512", - .main_type = KEXTYPE_ECDH, - .hash = &ssh_sha512, - .ecdh_vt = &ssh_ntru_selector_vt, -}; - -static const ssh_kex *const hybrid_list[] = { - &ssh_ntru_curve25519, - &ssh_ntru_curve25519_openssh, -}; - -const ssh_kexes ssh_ntru_hybrid_kex = { lenof(hybrid_list), hybrid_list }; diff --git a/defs.h b/defs.h index d8bfe02a..286436e9 100644 --- a/defs.h +++ b/defs.h @@ -187,6 +187,8 @@ typedef struct ssh2_ciphers ssh2_ciphers; typedef struct dh_ctx dh_ctx; typedef struct ecdh_key ecdh_key; typedef struct ecdh_keyalg ecdh_keyalg; +typedef struct pq_kemalg pq_kemalg; +typedef struct pq_kem_dk pq_kem_dk; typedef struct NTRUKeyPair NTRUKeyPair; typedef struct NTRUEncodeSchedule NTRUEncodeSchedule; typedef struct RFC6979 RFC6979; diff --git a/ssh.h b/ssh.h index 46356f0c..c3ebcc68 100644 --- a/ssh.h +++ b/ssh.h @@ -1005,6 +1005,52 @@ static inline bool ecdh_key_getkey(ecdh_key *key, ptrlen remoteKey, static inline char *ecdh_keyalg_description(const ssh_kex *kex) { return kex->ecdh_vt->description(kex); } +/* + * vtable for post-quantum key encapsulation methods (things like NTRU + * and ML-KEM). + * + * These work in an asymmetric way that's conceptually more like the + * old RSA kex (either SSH-1 or SSH-2) than like Diffie-Hellman. One + * party generates a keypair and sends the public key; the other party + * invents a secret and encrypts it with the public key; the first + * party receives the ciphertext and decrypts it, and now both parties + * have the secret. + */ +struct pq_kem_dk { + const pq_kemalg *vt; +}; +struct pq_kemalg { + /* Generate a key pair, writing the public encryption key in wire + * format to ek. Return the decryption key. */ + pq_kem_dk *(*keygen)(const pq_kemalg *alg, BinarySink *ek); + /* Invent and encrypt a secret, writing the ciphertext in wire + * format to c and the secret itself to k. Returns false on any + * kind of really obvious validation failure of the encryption key. */ + bool (*encaps)(const pq_kemalg *alg, BinarySink *c, BinarySink *k, + ptrlen ek); + /* Decrypt the secret and write it to k. Returns false on + * validation failure. However, more competent cryptographic + * attacks are rejected in a way that's not obvious, returning a + * useless pseudorandom secret. */ + bool (*decaps)(pq_kem_dk *dk, BinarySink *k, ptrlen c); + /* Free a decryption key. */ + void (*free_dk)(pq_kem_dk *dk); + + const void *extra; + const char *description; + size_t ek_len, c_len; +}; + +static inline pq_kem_dk *pq_kem_keygen(const pq_kemalg *alg, BinarySink *ek) +{ return alg->keygen(alg, ek); } +static inline bool pq_kem_encaps(const pq_kemalg *alg, BinarySink *c, + BinarySink *k, ptrlen ek) +{ return alg->encaps(alg, c, k, ek); } +static inline bool pq_kem_decaps(pq_kem_dk *dk, BinarySink *k, ptrlen c) +{ return dk->vt->decaps(dk, k, c); } +static inline void pq_kem_free_dk(pq_kem_dk *dk) +{ dk->vt->free_dk(dk); } + /* * Suffix on GSSAPI SSH protocol identifiers that indicates Kerberos 5 * as the mechanism. @@ -1194,6 +1240,7 @@ extern const ssh_kex ssh_ec_kex_nistp384; extern const ssh_kex ssh_ec_kex_nistp521; extern const ssh_kexes ssh_ecdh_kex; extern const ssh_kexes ssh_ntru_hybrid_kex; +extern const pq_kemalg ssh_ntru; extern const ssh_keyalg ssh_dsa; extern const ssh_keyalg ssh_rsa; extern const ssh_keyalg ssh_rsa_sha256;