mirror of
https://git.tartarus.org/simon/putty.git
synced 2025-01-09 01:18: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:
parent
fcdc804b4f
commit
f08da2b638
@ -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
|
||||
|
322
crypto/kex-hybrid.c
Normal file
322
crypto/kex-hybrid.c
Normal 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,
|
||||
};
|
472
crypto/ntru.c
472
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 };
|
||||
|
2
defs.h
2
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;
|
||||
|
47
ssh.h
47
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;
|
||||
|
Loading…
Reference in New Issue
Block a user