Skip to content

Commit

Permalink
Add general post-quantum hybrid key exchange
Browse files Browse the repository at this point in the history
This implements draft-kampanakis-curdle-ssh-pq-ke-04, a
mechanism combining a post-quantum KEM with x25519.

Adding a post-quantum KEM can avoid harvest-now-decrypt-later style
attacks (captured traffic decrypted in future by a quantum computer, if
they are created). Combining it with existing x25519 ensures that
security is not weakened compared to the present status, if a weakness
in the PQ KEM is discovered.

A future commit will add concrete PQ methods, currently this code
is unused.
x25519 could be generalised later if needed.
  • Loading branch information
mkj committed Dec 14, 2024
1 parent 0979e17 commit 9051b69
Show file tree
Hide file tree
Showing 15 changed files with 280 additions and 30 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ jobs:
#define DROPBEAR_SHA2_512_HMAC 1
#define DROPBEAR_CLI_PUBKEY_AUTH 0
- name: pq, no plain x25519
localoptions: |
#define DROPBEAR_CURVE25519 0
# # Fuzzers run standalone. A bit superfluous with cifuzz, but
# # good to run the whole corpus to keep it working.
# - name: fuzzing with address sanitizer
Expand Down
2 changes: 1 addition & 1 deletion Makefile.in
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ _CLISVROBJS=common-session.o packet.o common-algo.o common-kex.o \
common-channel.o common-chansession.o termcodes.o loginrec.o \
tcp-accept.o listener.o process-packet.o dh_groups.o \
common-runopts.o circbuffer.o list.o netio.o chachapoly.o gcm.o \
kex-x25519.o kex-dh.o kex-ecdh.o
kex-x25519.o kex-dh.o kex-ecdh.o kex-pqhybrid.o
CLISVROBJS = $(patsubst %,$(OBJ_DIR)/%,$(_CLISVROBJS))

_KEYOBJS=dropbearkey.o
Expand Down
22 changes: 15 additions & 7 deletions src/algo.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ enum dropbear_kex_mode {
#if DROPBEAR_CURVE25519
DROPBEAR_KEX_CURVE25519,
#endif
#if DROPBEAR_PQHYBRID
DROPBEAR_KEX_PQHYBRID,
#endif
};

struct dropbear_kex {
Expand All @@ -109,18 +112,23 @@ struct dropbear_kex {
const unsigned char *dh_p_bytes;
const int dh_p_len;

/* elliptic curve DH KEX */
#if DROPBEAR_ECDH
const struct dropbear_ecc_curve *ecc_curve;
#else
/* NULL for non-ecc curves */
const void* dummy;
#endif
/* kex specific, could be ecc_curve or pqhybrid_desc */
const void* details;

/* both */
const struct ltc_hash_descriptor *hash_desc;
};

struct dropbear_kem_desc {
unsigned int public_len;
unsigned int secret_len;
unsigned int ciphertext_len;
unsigned int output_len;
int (*kem_gen)(unsigned char *pk, unsigned char *sk);
int (*kem_enc)(unsigned char *c, unsigned char *k, const unsigned char *pk);
int (*kem_dec)(unsigned char *k, const unsigned char *c, const unsigned char *sk);
};

/* Includes all algorithms is useall is set */
void buf_put_algolist_all(buffer * buf, const algo_type localalgos[], int useall);
/* Includes "usable" algorithms */
Expand Down
9 changes: 9 additions & 0 deletions src/buffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,15 @@ buffer * buf_getbuf(buffer *buf) {
return buf_getstringbuf_int(buf, 1);
}

/* Returns the equivalent of buf_getptr() as a new buffer. */
buffer * buf_getptrcopy(const buffer* buf, unsigned int len) {
unsigned char *src = buf_getptr(buf, len);
buffer *ret = buf_new(len);
buf_putbytes(ret, src, len);
buf_setpos(ret, 0);
return ret;
}

/* Just increment the buffer position the same as if we'd used buf_getstring,
* but don't bother copying/malloc()ing for it */
void buf_eatstring(buffer *buf) {
Expand Down
1 change: 1 addition & 0 deletions src/buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ unsigned char* buf_getwriteptr(const buffer* buf, unsigned int len);
char* buf_getstring(buffer* buf, unsigned int *retlen);
buffer * buf_getstringbuf(buffer *buf);
buffer * buf_getbuf(buffer *buf);
buffer * buf_getptrcopy(const buffer* buf, unsigned int len);
void buf_eatstring(buffer *buf);
void buf_putint(buffer* buf, unsigned int val);
void buf_putstring(buffer* buf, const char* str, unsigned int len);
Expand Down
15 changes: 15 additions & 0 deletions src/cli-kex.c
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ void send_msg_kexdh_init() {
cli_ses.curve25519_param = gen_kexcurve25519_param();
buf_putstring(ses.writepayload, cli_ses.curve25519_param->pub, CURVE25519_LEN);
break;
#endif
#if DROPBEAR_PQHYBRID
case DROPBEAR_KEX_PQHYBRID:
cli_ses.pqhybrid_param = gen_kexpqhybrid_param();
buf_putbufstring(ses.writepayload, cli_ses.pqhybrid_param->concat_public);
break;
#endif
}

Expand Down Expand Up @@ -170,6 +176,15 @@ void recv_msg_kexdh_reply() {
buf_free(ecdh_qs);
}
break;
#endif
#if DROPBEAR_PQHYBRID
case DROPBEAR_KEX_PQHYBRID:
{
buffer *ecdh_qs = buf_getstringbuf(ses.payload);
kexpqhybrid_comb_key(cli_ses.pqhybrid_param, ecdh_qs, hostkey);
buf_free(ecdh_qs);
}
break;
#endif
}

Expand Down
13 changes: 10 additions & 3 deletions src/common-kex.c
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,16 @@ static void gen_new_keys() {
/* the dh_K and hash are the start of all hashes, we make use of that */

hash_desc->init(&hs);
hash_process_mp(hash_desc, &hs, ses.dh_K);
mp_clear(ses.dh_K);
m_free(ses.dh_K);
if (ses.dh_K) {
hash_process_mp(hash_desc, &hs, ses.dh_K);
mp_clear(ses.dh_K);
m_free(ses.dh_K);
}
if (ses.dh_K_bytes) {
hash_desc->process(&hs, ses.dh_K_bytes->data, ses.dh_K_bytes->len);
buf_burn_free(ses.dh_K_bytes);
ses.dh_K_bytes = NULL;
}
hash_desc->process(&hs, ses.hash->data, ses.hash->len);
buf_burn_free(ses.hash);
ses.hash = NULL;
Expand Down
12 changes: 6 additions & 6 deletions src/curve25519.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
#include "dbrandom.h"
#include "curve25519.h"

#if DROPBEAR_CURVE25519 || DROPBEAR_ED25519
#if DROPBEAR_CURVE25519_DEP || DROPBEAR_ED25519

/* Modified TweetNaCl version 20140427, a self-contained public-domain C library.
* https://tweetnacl.cr.yp.to/ */
Expand All @@ -40,10 +40,10 @@ typedef unsigned long long u64;
typedef long long i64;
typedef i64 gf[16];

#if DROPBEAR_CURVE25519
#if DROPBEAR_CURVE25519_DEP
static const gf
_121665 = {0xDB41,1};
#endif /* DROPBEAR_CURVE25519 */
#endif /* DROPBEAR_CURVE25519_DEP */
#if DROPBEAR_ED25519
static const gf
gf0,
Expand Down Expand Up @@ -207,7 +207,7 @@ sv pow2523(gf o,const gf i)
}
#endif /* DROPBEAR_ED25519 && DROPBEAR_SIGNKEY_VERIFY */

#if DROPBEAR_CURVE25519
#if DROPBEAR_CURVE25519_DEP
void dropbear_curve25519_scalarmult(u8 *q,const u8 *n,const u8 *p)
{
u8 z[32];
Expand Down Expand Up @@ -257,7 +257,7 @@ void dropbear_curve25519_scalarmult(u8 *q,const u8 *n,const u8 *p)
M(x+16,x+16,x+32);
pack25519(q,x+16);
}
#endif /* DROPBEAR_CURVE25519 */
#endif /* DROPBEAR_CURVE25519_DEP */

#if DROPBEAR_ED25519
static int crypto_hash(u8 *out,const u8 *m,u64 n)
Expand Down Expand Up @@ -494,4 +494,4 @@ int dropbear_ed25519_verify(const u8 *m,u32 mlen,const u8 *s,u32 slen,const u8 *

#endif /* DROPBEAR_ED25519 */

#endif /* DROPBEAR_CURVE25519 || DROPBEAR_ED25519 */
#endif /* DROPBEAR_CURVE25519_DEP || DROPBEAR_ED25519 */
8 changes: 5 additions & 3 deletions src/kex-ecdh.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
#if DROPBEAR_ECDH
struct kex_ecdh_param *gen_kexecdh_param() {
struct kex_ecdh_param *param = m_malloc(sizeof(*param));
const struct dropbear_ecc_curve *curve = ses.newkeys->algo_kex->details;
if (ecc_make_key_ex(NULL, dropbear_ltc_prng,
&param->key, ses.newkeys->algo_kex->ecc_curve->dp) != CRYPT_OK) {
&param->key, curve->dp) != CRYPT_OK) {
dropbear_exit("ECC error");
}
return param;
Expand All @@ -24,11 +25,12 @@ void free_kexecdh_param(struct kex_ecdh_param *param) {
}
void kexecdh_comb_key(struct kex_ecdh_param *param, buffer *pub_them,
sign_key *hostkey) {
const struct dropbear_kex *algo_kex = ses.newkeys->algo_kex;
const struct dropbear_ecc_curve *curve
= ses.newkeys->algo_kex->details;
/* public keys from client and server */
ecc_key *Q_C, *Q_S, *Q_them;

Q_them = buf_get_ecc_raw_pubkey(pub_them, algo_kex->ecc_curve);
Q_them = buf_get_ecc_raw_pubkey(pub_them, curve);
if (Q_them == NULL) {
dropbear_exit("ECC error");
}
Expand Down
142 changes: 142 additions & 0 deletions src/kex-pqhybrid.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#include "includes.h"
#include "algo.h"
#include "buffer.h"
#include "session.h"
#include "bignum.h"
#include "dbrandom.h"
#include "crypto_desc.h"
#include "curve25519.h"
#include "kex.h"

#if DROPBEAR_PQHYBRID

struct kex_pqhybrid_param *gen_kexpqhybrid_param() {
struct kex_pqhybrid_param *param = m_malloc(sizeof(*param));
const struct dropbear_kem_desc *kem = ses.newkeys->algo_kex->details;

param->curve25519 = gen_kexcurve25519_param();

if (IS_DROPBEAR_CLIENT) {
param->kem_cli_secret = buf_new(kem->secret_len);
param->concat_public = buf_new(kem->public_len + CURVE25519_LEN);
kem->kem_gen(
buf_getwriteptr(param->concat_public, kem->public_len),
buf_getwriteptr(param->kem_cli_secret, kem->secret_len));
buf_incrwritepos(param->concat_public, kem->public_len);
buf_incrwritepos(param->kem_cli_secret, kem->secret_len);
buf_setpos(param->kem_cli_secret, 0);
/* Append the curve25519 parameter */
buf_putbytes(param->concat_public, param->curve25519->pub, CURVE25519_LEN);
}

return param;
}

void free_kexpqhybrid_param(struct kex_pqhybrid_param *param) {
free_kexcurve25519_param(param->curve25519);
if (param->kem_cli_secret) {
buf_burn_free(param->kem_cli_secret);
param->kem_cli_secret = NULL;
}
buf_free(param->concat_public);
m_free(param);
}

void kexpqhybrid_comb_key(struct kex_pqhybrid_param *param,
buffer *buf_pub, sign_key *hostkey) {

const struct dropbear_kem_desc *kem = ses.newkeys->algo_kex->details;
const struct ltc_hash_descriptor *hash_desc
= ses.newkeys->algo_kex->hash_desc;

/* Either public key (from client) or ciphertext (from server) */
unsigned char *remote_pub_kem = NULL;
buffer *pub_25519 = NULL;
buffer *k_out = NULL;
unsigned int remote_len;
hash_state hs;
const buffer * Q_C = NULL;
const buffer * Q_S = NULL;

/* Extract input parts from the remote peer */
if (IS_DROPBEAR_CLIENT) {
/* S_REPLY = S_CT2 || S_PK1 */
remote_len = kem->ciphertext_len;
} else {
/* C_INIT = C_PK2 || C_PK1 */
remote_len = kem->public_len;
}
remote_pub_kem = buf_getptr(buf_pub, remote_len);
buf_incrpos(buf_pub, remote_len);
pub_25519 = buf_getptrcopy(buf_pub, CURVE25519_LEN);
buf_incrpos(buf_pub, CURVE25519_LEN);
/* Check all is consumed */
if (buf_pub->pos != buf_pub->len) {
dropbear_exit("Bad sntrup");
}

/* k_out = K_PQ || K_CL */
k_out = buf_new(kem->output_len + CURVE25519_LEN);

/* Derive pq kem part (K_PQ) */
if (IS_DROPBEAR_CLIENT) {
kem->kem_dec(
buf_getwriteptr(k_out, kem->output_len),
remote_pub_kem,
buf_getptr(param->kem_cli_secret, kem->secret_len));
buf_burn_free(param->kem_cli_secret);
param->kem_cli_secret = NULL;
} else {
/* Server returns ciphertext */
assert(param->concat_public == NULL);
param->concat_public = buf_new(kem->ciphertext_len + CURVE25519_LEN);
kem->kem_enc(
buf_getwriteptr(param->concat_public, kem->ciphertext_len),
buf_getwriteptr(k_out, kem->output_len),
remote_pub_kem);
buf_incrwritepos(param->concat_public, kem->ciphertext_len);
/* Append the curve25519 parameter */
buf_putbytes(param->concat_public, param->curve25519->pub, CURVE25519_LEN);
}
buf_incrwritepos(k_out, kem->output_len);

/* Derive ec part (K_CL) */
kexcurve25519_derive(param->curve25519, pub_25519,
buf_getwriteptr(k_out, CURVE25519_LEN));
buf_incrwritepos(k_out, CURVE25519_LEN);

/* dh_K_bytes = HASH(k_out)
dh_K_bytes is a SSH string with length prefix, since
that is what needs to be hashed in gen_new_keys() */
ses.dh_K_bytes = buf_new(4 + hash_desc->hashsize);
buf_putint(ses.dh_K_bytes, hash_desc->hashsize);
hash_desc->init(&hs);
hash_desc->process(&hs, k_out->data, k_out->len);
hash_desc->done(&hs, buf_getwriteptr(ses.dh_K_bytes, hash_desc->hashsize));
m_burn(&hs, sizeof(hash_state));
buf_incrwritepos(ses.dh_K_bytes, hash_desc->hashsize);

/* Create the remainder of the hash buffer */
if (IS_DROPBEAR_CLIENT) {
Q_C = param->concat_public;
Q_S = buf_pub;
} else {
Q_S = param->concat_public;
Q_C = buf_pub;
}

/* K_S, the host key */
buf_put_pub_key(ses.kexhashbuf, hostkey, ses.newkeys->algo_hostkey);
buf_putbufstring(ses.kexhashbuf, Q_C);
buf_putbufstring(ses.kexhashbuf, Q_S);
/* K, the shared secret */
buf_putbytes(ses.kexhashbuf, ses.dh_K_bytes->data, ses.dh_K_bytes->len);

/* calculate the hash H to sign */
finish_kexhashbuf();

buf_burn_free(k_out);
buf_free(pub_25519);
}

#endif /* DROPBEAR_PQHYBRID */
26 changes: 19 additions & 7 deletions src/kex-x25519.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
#include "curve25519.h"
#include "kex.h"

#if DROPBEAR_CURVE25519
/* PQ hybrids also use curve25519 internally */
#if DROPBEAR_CURVE25519_DEP

struct kex_curve25519_param *gen_kexcurve25519_param() {
/* Per http://cr.yp.to/ecdh.html */
Expand All @@ -26,13 +27,10 @@ void free_kexcurve25519_param(struct kex_curve25519_param *param) {
m_free(param);
}

void kexcurve25519_comb_key(const struct kex_curve25519_param *param, const buffer *buf_pub_them,
sign_key *hostkey) {
unsigned char out[CURVE25519_LEN];
const unsigned char* Q_C = NULL;
const unsigned char* Q_S = NULL;
/* out must be CURVE25519_LEN */
void kexcurve25519_derive(const struct kex_curve25519_param *param, const buffer *buf_pub_them,
unsigned char *out) {
char zeroes[CURVE25519_LEN] = {0};

if (buf_pub_them->len != CURVE25519_LEN)
{
dropbear_exit("Bad curve25519");
Expand All @@ -43,6 +41,20 @@ void kexcurve25519_comb_key(const struct kex_curve25519_param *param, const buff
if (constant_time_memcmp(zeroes, out, CURVE25519_LEN) == 0) {
dropbear_exit("Bad curve25519");
}
}

#endif /* DROPBEAR_CURVE25519_DEP */

#if DROPBEAR_CURVE25519

/* Only required for x25519 directly */
void kexcurve25519_comb_key(const struct kex_curve25519_param *param, const buffer *buf_pub_them,
sign_key *hostkey) {
unsigned char out[CURVE25519_LEN];
const unsigned char* Q_C = NULL;
const unsigned char* Q_S = NULL;

kexcurve25519_derive(param, buf_pub_them, out);

m_mp_alloc_init_multi(&ses.dh_K, NULL);
bytes_to_mp(ses.dh_K, out, CURVE25519_LEN);
Expand Down
Loading

0 comments on commit 9051b69

Please sign in to comment.