1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-09 17:38:00 +00:00

Separate NTRU Prime from the hybridisation layer.

Now ntru.c contains just the NTRU business, and kex-hybrid.c contains
the system for running a post-quantum and a classical KEX and hashing
together the results. In between them is a new small vtable API for
the key encapsulation mechanisms that the post-quantum standardisation
effort seems to be settling on.
This commit is contained in:
Simon Tatham 2024-12-07 19:21:57 +00:00
parent fcdc804b4f
commit f08da2b638
5 changed files with 474 additions and 370 deletions

View File

@ -20,6 +20,7 @@ add_sources_from_current_dir(crypto
ecc-ssh.c ecc-ssh.c
hash_simple.c hash_simple.c
hmac.c hmac.c
kex-hybrid.c
mac.c mac.c
mac_simple.c mac_simple.c
md5.c md5.c

322
crypto/kex-hybrid.c Normal file
View File

@ -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 <stdio.h>
#include <stdlib.h>
#include <assert.h>
#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,
};

View File

@ -1432,14 +1432,7 @@ static void ntru_session_hash(
} }
/* ---------------------------------------------------------------------- /* ----------------------------------------------------------------------
* Top-level key exchange and SSH integration. * Top-level KEM functions.
*
* 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.
*/ */
/* /*
@ -1451,257 +1444,30 @@ static void ntru_session_hash(
#define q_LIVE 4591 #define q_LIVE 4591
#define w_LIVE 286 #define w_LIVE 286
static char *ssh_ntru_description(const ssh_kex *kex) struct ntru_dk {
{
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 {
NTRUKeyPair *keypair; NTRUKeyPair *keypair;
ecdh_key *curve25519; strbuf *encoded;
pq_kem_dk dk;
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,
}; };
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); struct ntru_dk *ndk = snew(struct ntru_dk);
nk->ek.vt = &ssh_ntru_client_vt; ndk->dk.vt = alg;
ndk->encoded = strbuf_new_nm();
nk->keypair = ntru_keygen(p_LIVE, q_LIVE, w_LIVE); ndk->keypair = ntru_keygen(p_LIVE, q_LIVE, w_LIVE);
nk->curve25519 = ecdh_key_new(&ssh_ec_kex_curve25519, false); ntru_encode_pubkey(ndk->keypair->h, p_LIVE, q_LIVE, ek);
return &ndk->dk;
return &nk->ek;
} }
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 src[1];
BinarySource_BARE_INIT_PL(src, remoteKey); BinarySource_BARE_INIT_PL(src, ek);
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);
uint16_t *pubkey = snewn(p_LIVE, uint16_t); uint16_t *pubkey = snewn(p_LIVE, uint16_t);
ntru_decode_pubkey(pubkey, p_LIVE, q_LIVE, src); ntru_decode_pubkey(pubkey, p_LIVE, q_LIVE, src);
ptrlen curve25519_remoteKey = get_data(src, 32);
if (get_err(src) || get_avail(src)) { if (get_err(src) || get_avail(src)) {
/* Hard-fail if the input wasn't exactly the right length */ /* 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; return false;
} }
/* /* Invent a valid NTRU plaintext. */
* Main hash object which will combine the NTRU and Curve25519 uint16_t *plaintext = snewn(p_LIVE, uint16_t);
* outputs. ntru_gen_short(plaintext, p_LIVE, w_LIVE);
*/
ssh_hash *h = ssh_hash_new(&ssh_sha512);
/* Reusable buffer for storing various hash outputs. */ /* Encrypt the plaintext, and encode the ciphertext into a strbuf,
uint8_t hashdata[64]; * 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));
/* /* Compute the confirmation hash, and append that to the data sent
* NTRU side. * to the other side. */
*/ uint8_t confhash[32];
{ ntru_confirmation_hash(confhash, plaintext, pubkey, p_LIVE, q_LIVE);
/* Encrypt the plaintext we generated at construction time, put_data(c, confhash, 32);
* 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 write it into another /* Compute the session hash, i.e. the output shared secret. */
* strbuf. */ uint8_t sesshash[32];
ntru_confirmation_hash(hashdata, nk->plaintext, pubkey, ntru_session_hash(sesshash, 1, plaintext, p_LIVE,
p_LIVE, q_LIVE); ptrlen_from_strbuf(ciphertext_encoded),
put_data(nk->confirmation_hash, hashdata, 32); make_ptrlen(confhash, 32));
put_data(k, sesshash, 32);
/* Compute the session hash (which is easy on the server side, ring_free(pubkey, p_LIVE);
* requiring no conditional substitution). */ ring_free(plaintext, p_LIVE);
ntru_session_hash(hashdata, 1, nk->plaintext, p_LIVE, ring_free(ciphertext, p_LIVE);
ptrlen_from_strbuf(nk->ciphertext_encoded), strbuf_free(ciphertext_encoded);
ptrlen_from_strbuf(nk->confirmation_hash)); smemclr(confhash, sizeof(confhash));
smemclr(sesshash, sizeof(sesshash));
/* 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));
return true; 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);
/* /* Expect a string containing a ciphertext and a confirmation hash. */
* In the server, this function is called after getkey, so we BinarySource src[1];
* already have all our pieces prepared. Just concatenate them all BinarySource_BARE_INIT_PL(src, c);
* into the 'server's public data' string to go in ECDH_REPLY.
*/ uint16_t *ciphertext = snewn(p_LIVE, uint16_t);
put_datapl(bs, ptrlen_from_strbuf(nk->ciphertext_encoded)); ptrlen ciphertext_encoded = ntru_decode_ciphertext(
put_datapl(bs, ptrlen_from_strbuf(nk->confirmation_hash)); ciphertext, ndk->keypair, src);
ecdh_key_getpublic(nk->curve25519, bs); 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;
} }
/* ---------------------------------------------------------------------- static void ntru_vt_free_dk(pq_kem_dk *dk)
* 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)
{ {
if (is_server) struct ntru_dk *ndk = container_of(dk, struct ntru_dk, dk);
return ssh_ntru_server_new(); strbuf_free(ndk->encoded);
else ntru_keypair_free(ndk->keypair);
return ssh_ntru_client_new(); sfree(ndk);
} }
static const ecdh_keyalg ssh_ntru_selector_vt = { const pq_kemalg ssh_ntru = {
/* This is a never-instantiated vtable which only implements the .keygen = ntru_vt_keygen,
* functions that don't require an instance. */ .encaps = ntru_vt_encaps,
.new = ssh_ntru_new, .decaps = ntru_vt_decaps,
.description = ssh_ntru_description, .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 };

2
defs.h
View File

@ -187,6 +187,8 @@ typedef struct ssh2_ciphers ssh2_ciphers;
typedef struct dh_ctx dh_ctx; typedef struct dh_ctx dh_ctx;
typedef struct ecdh_key ecdh_key; typedef struct ecdh_key ecdh_key;
typedef struct ecdh_keyalg ecdh_keyalg; 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 NTRUKeyPair NTRUKeyPair;
typedef struct NTRUEncodeSchedule NTRUEncodeSchedule; typedef struct NTRUEncodeSchedule NTRUEncodeSchedule;
typedef struct RFC6979 RFC6979; typedef struct RFC6979 RFC6979;

47
ssh.h
View File

@ -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) static inline char *ecdh_keyalg_description(const ssh_kex *kex)
{ return kex->ecdh_vt->description(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 * Suffix on GSSAPI SSH protocol identifiers that indicates Kerberos 5
* as the mechanism. * 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_kex ssh_ec_kex_nistp521;
extern const ssh_kexes ssh_ecdh_kex; extern const ssh_kexes ssh_ecdh_kex;
extern const ssh_kexes ssh_ntru_hybrid_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_dsa;
extern const ssh_keyalg ssh_rsa; extern const ssh_keyalg ssh_rsa;
extern const ssh_keyalg ssh_rsa_sha256; extern const ssh_keyalg ssh_rsa_sha256;