Address validation using Retry packets.

The behaviour is toggled with the new directive "quic_retry on|off".
QUIC token construction is made suitable for issuing with NEW_TOKEN.
This commit is contained in:
Sergey Kandaurov 2020-05-14 15:47:18 +03:00
parent 92324d157c
commit fbff14f583
6 changed files with 497 additions and 9 deletions

View file

@ -123,6 +123,7 @@ struct ngx_quic_connection_s {
unsigned closing:1;
unsigned draining:1;
unsigned key_phase:1;
unsigned in_retry:1;
};
@ -154,6 +155,10 @@ static ngx_int_t ngx_quic_new_connection(ngx_connection_t *c, ngx_ssl_t *ssl,
ngx_quic_tp_t *tp, ngx_quic_header_t *pkt,
ngx_connection_handler_pt handler);
static ngx_int_t ngx_quic_new_dcid(ngx_connection_t *c, ngx_str_t *odcid);
static ngx_int_t ngx_quic_retry(ngx_connection_t *c);
static ngx_int_t ngx_quic_new_token(ngx_connection_t *c, ngx_str_t *token);
static ngx_int_t ngx_quic_validate_token(ngx_connection_t *c,
ngx_quic_header_t *pkt);
static ngx_int_t ngx_quic_init_connection(ngx_connection_t *c);
static void ngx_quic_input_handler(ngx_event_t *rev);
@ -165,6 +170,8 @@ static ngx_int_t ngx_quic_close_streams(ngx_connection_t *c,
static ngx_int_t ngx_quic_input(ngx_connection_t *c, ngx_buf_t *b);
static ngx_inline u_char *ngx_quic_skip_zero_padding(ngx_buf_t *b);
static ngx_int_t ngx_quic_retry_input(ngx_connection_t *c,
ngx_quic_header_t *pkt);
static ngx_int_t ngx_quic_initial_input(ngx_connection_t *c,
ngx_quic_header_t *pkt);
static ngx_int_t ngx_quic_handshake_input(ngx_connection_t *c,
@ -524,7 +531,8 @@ ngx_quic_run(ngx_connection_t *c, ngx_ssl_t *ssl, ngx_quic_tp_t *tp,
return;
}
ngx_add_timer(c->read, c->quic->tp.max_idle_timeout);
ngx_add_timer(c->read, c->quic->in_retry ? NGX_QUIC_RETRY_TIMEOUT
: c->quic->tp.max_idle_timeout);
c->read->handler = ngx_quic_input_handler;
@ -625,13 +633,6 @@ ngx_quic_new_connection(ngx_connection_t *c, ngx_ssl_t *ssl, ngx_quic_tp_t *tp,
}
ngx_memcpy(qc->scid.data, pkt->scid.data, qc->scid.len);
qc->token.len = pkt->token.len;
qc->token.data = ngx_pnalloc(c->pool, qc->token.len);
if (qc->token.data == NULL) {
return NGX_ERROR;
}
ngx_memcpy(qc->token.data, pkt->token.data, qc->token.len);
keys = &c->quic->keys[ssl_encryption_initial];
if (ngx_quic_set_initial_secret(c->pool, &keys->client, &keys->server,
@ -641,6 +642,10 @@ ngx_quic_new_connection(ngx_connection_t *c, ngx_ssl_t *ssl, ngx_quic_tp_t *tp,
return NGX_ERROR;
}
if (tp->retry) {
return ngx_quic_retry(c);
}
pkt->secret = &keys->client;
pkt->level = ssl_encryption_initial;
pkt->plaintext = buf;
@ -706,6 +711,270 @@ ngx_quic_new_dcid(ngx_connection_t *c, ngx_str_t *odcid)
}
static ngx_int_t
ngx_quic_retry(ngx_connection_t *c)
{
ssize_t len;
ngx_str_t res, token;
ngx_quic_header_t pkt;
u_char buf[NGX_QUIC_RETRY_BUFFER_SIZE];
if (ngx_quic_new_token(c, &token) != NGX_OK) {
return NGX_ERROR;
}
ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
pkt.flags = NGX_QUIC_PKT_FIXED_BIT | NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_RETRY;
pkt.log = c->log;
pkt.odcid = c->quic->odcid;
pkt.dcid = c->quic->scid;
pkt.scid = c->quic->dcid;
pkt.token = token;
res.data = buf;
if (ngx_quic_encrypt(&pkt, NULL, &res) != NGX_OK) {
return NGX_ERROR;
}
#ifdef NGX_QUIC_DEBUG_PACKETS
ngx_quic_hexdump(c->log, "quic packet to send", res.data, res.len);
#endif
len = c->send(c, res.data, res.len);
if (len == NGX_ERROR || (size_t) len != res.len) {
return NGX_ERROR;
}
c->quic->token = token;
c->quic->tp.original_connection_id = c->quic->odcid;
c->quic->in_retry = 1;
return NGX_OK;
}
static ngx_int_t
ngx_quic_new_token(ngx_connection_t *c, ngx_str_t *token)
{
int len, iv_len;
u_char *data, *p, *key, *iv;
ngx_msec_t now;
EVP_CIPHER_CTX *ctx;
const EVP_CIPHER *cipher;
struct sockaddr_in *sin;
#if (NGX_HAVE_INET6)
struct sockaddr_in6 *sin6;
#endif
u_char in[NGX_QUIC_MAX_TOKEN_SIZE];
switch (c->sockaddr->sa_family) {
#if (NGX_HAVE_INET6)
case AF_INET6:
sin6 = (struct sockaddr_in6 *) c->sockaddr;
len = sizeof(struct in6_addr);
data = sin6->sin6_addr.s6_addr;
break;
#endif
#if (NGX_HAVE_UNIX_DOMAIN)
case AF_UNIX:
len = ngx_min(c->addr_text.len, NGX_QUIC_MAX_TOKEN_SIZE - sizeof(now));
data = c->addr_text.data;
break;
#endif
default: /* AF_INET */
sin = (struct sockaddr_in *) c->sockaddr;
len = sizeof(in_addr_t);
data = (u_char *) &sin->sin_addr;
break;
}
p = ngx_cpymem(in, data, len);
now = ngx_current_msec;
len += sizeof(now);
ngx_memcpy(p, &now, sizeof(now));
cipher = EVP_aes_256_cbc();
iv_len = EVP_CIPHER_iv_length(cipher);
token->len = iv_len + len + EVP_CIPHER_block_size(cipher);
token->data = ngx_pnalloc(c->pool, token->len);
if (token->data == NULL) {
return NGX_ERROR;
}
ctx = EVP_CIPHER_CTX_new();
if (ctx == NULL) {
return NGX_ERROR;
}
key = c->quic->tp.token_key;
iv = token->data;
if (RAND_bytes(iv, iv_len) <= 0
|| !EVP_EncryptInit_ex(ctx, cipher, NULL, key, iv))
{
EVP_CIPHER_CTX_free(ctx);
return NGX_ERROR;
}
token->len = iv_len;
if (EVP_EncryptUpdate(ctx, token->data + token->len, &len, in, len) != 1) {
EVP_CIPHER_CTX_free(ctx);
return NGX_ERROR;
}
token->len += len;
if (EVP_EncryptFinal_ex(ctx, token->data + token->len, &len) <= 0) {
EVP_CIPHER_CTX_free(ctx);
return NGX_ERROR;
}
token->len += len;
EVP_CIPHER_CTX_free(ctx);
#ifdef NGX_QUIC_DEBUG_PACKETS
ngx_quic_hexdump(c->log, "quic new token", token->data, token->len);
#endif
return NGX_OK;
}
static ngx_int_t
ngx_quic_validate_token(ngx_connection_t *c, ngx_quic_header_t *pkt)
{
int len, tlen, iv_len;
u_char *key, *iv, *p, *data;
ngx_msec_t msec;
EVP_CIPHER_CTX *ctx;
const EVP_CIPHER *cipher;
struct sockaddr_in *sin;
#if (NGX_HAVE_INET6)
struct sockaddr_in6 *sin6;
#endif
ngx_quic_connection_t *qc;
u_char tdec[NGX_QUIC_MAX_TOKEN_SIZE];
if (pkt->token.len == 0) {
return NGX_ERROR;
}
qc = c->quic;
/* Retry token */
if (qc->token.len) {
if (pkt->token.len != qc->token.len) {
return NGX_ERROR;
}
if (ngx_memcmp(pkt->token.data, qc->token.data, pkt->token.len) != 0) {
return NGX_ERROR;
}
return NGX_OK;
}
/* NEW_TOKEN in a previous connection */
cipher = EVP_aes_256_cbc();
key = c->quic->tp.token_key;
iv = pkt->token.data;
iv_len = EVP_CIPHER_iv_length(cipher);
/* sanity checks */
if (pkt->token.len < (size_t) iv_len + EVP_CIPHER_block_size(cipher)) {
return NGX_ERROR;
}
if (pkt->token.len > (size_t) iv_len + NGX_QUIC_MAX_TOKEN_SIZE) {
return NGX_ERROR;
}
ctx = EVP_CIPHER_CTX_new();
if (ctx == NULL) {
return NGX_ERROR;
}
if (!EVP_DecryptInit_ex(ctx, cipher, NULL, key, iv)) {
EVP_CIPHER_CTX_free(ctx);
return NGX_ERROR;
}
p = pkt->token.data + iv_len;
len = pkt->token.len - iv_len;
if (EVP_DecryptUpdate(ctx, tdec, &len, p, len) != 1) {
EVP_CIPHER_CTX_free(ctx);
return NGX_ERROR;
}
if (EVP_DecryptFinal_ex(ctx, tdec + len, &tlen) <= 0) {
EVP_CIPHER_CTX_free(ctx);
return NGX_ERROR;
}
EVP_CIPHER_CTX_free(ctx);
switch (c->sockaddr->sa_family) {
#if (NGX_HAVE_INET6)
case AF_INET6:
sin6 = (struct sockaddr_in6 *) c->sockaddr;
len = sizeof(struct in6_addr);
data = sin6->sin6_addr.s6_addr;
break;
#endif
#if (NGX_HAVE_UNIX_DOMAIN)
case AF_UNIX:
len = ngx_min(c->addr_text.len, NGX_QUIC_MAX_TOKEN_SIZE - sizeof(msec));
data = c->addr_text.data;
break;
#endif
default: /* AF_INET */
sin = (struct sockaddr_in *) c->sockaddr;
len = sizeof(in_addr_t);
data = (u_char *) &sin->sin_addr;
break;
}
if (ngx_memcmp(tdec, data, len) != 0) {
return NGX_ERROR;
}
ngx_memcpy(&msec, tdec + len, sizeof(msec));
if (ngx_current_msec - msec > NGX_QUIC_RETRY_LIFETIME) {
return NGX_DECLINED;
}
return NGX_OK;
}
static ngx_int_t
ngx_quic_init_connection(ngx_connection_t *c)
{
@ -776,6 +1045,7 @@ ngx_quic_input_handler(ngx_event_t *rev)
b.start = buf;
b.end = buf + sizeof(buf);
b.pos = b.last = b.start;
b.memory = 1;
c = rev->data;
qc = c->quic;
@ -1047,6 +1317,10 @@ ngx_quic_input(ngx_connection_t *c, ngx_buf_t *b)
pkt.log = c->log;
pkt.flags = p[0];
if (c->quic->in_retry) {
return ngx_quic_retry_input(c, &pkt);
}
/* TODO: check current state */
if (ngx_quic_long_pkt(pkt.flags)) {
@ -1110,6 +1384,93 @@ ngx_quic_skip_zero_padding(ngx_buf_t *b)
}
static ngx_int_t
ngx_quic_retry_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
{
ngx_quic_secrets_t *keys;
ngx_quic_send_ctx_t *ctx;
ngx_quic_connection_t *qc;
static u_char buf[NGX_QUIC_DEFAULT_MAX_PACKET_SIZE];
c->log->action = "retrying quic connection";
qc = c->quic;
if (ngx_buf_size(pkt->raw) < NGX_QUIC_MIN_INITIAL_SIZE) {
ngx_log_error(NGX_LOG_INFO, c->log, 0,
"quic UDP datagram is too small for initial packet");
return NGX_OK;
}
if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
return NGX_ERROR;
}
if (ngx_quic_pkt_zrtt(pkt->flags)) {
ngx_log_error(NGX_LOG_INFO, c->log, 0,
"quic discard inflight 0-RTT packet");
return NGX_OK;
}
if (!ngx_quic_pkt_in(pkt->flags)) {
ngx_log_error(NGX_LOG_INFO, c->log, 0,
"quic invalid initial packet: 0x%xi", pkt->flags);
return NGX_ERROR;
}
if (ngx_quic_parse_initial_header(pkt) != NGX_OK) {
return NGX_ERROR;
}
if (ngx_quic_new_dcid(c, &pkt->dcid) != NGX_OK) {
return NGX_ERROR;
}
qc = c->quic;
keys = &c->quic->keys[ssl_encryption_initial];
if (ngx_quic_set_initial_secret(c->pool, &keys->client, &keys->server,
&qc->odcid)
!= NGX_OK)
{
return NGX_ERROR;
}
c->quic->in_retry = 0;
if (ngx_quic_validate_token(c, pkt) != NGX_OK) {
ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic invalid token");
return NGX_ERROR;
}
pkt->secret = &keys->client;
pkt->level = ssl_encryption_initial;
pkt->plaintext = buf;
ctx = ngx_quic_get_send_ctx(qc, pkt->level);
if (ngx_quic_decrypt(pkt, NULL, &ctx->largest_pn) != NGX_OK) {
return NGX_ERROR;
}
if (ngx_quic_init_connection(c) != NGX_OK) {
return NGX_ERROR;
}
if (ngx_quic_payload_handler(c, pkt) != NGX_OK) {
return NGX_ERROR;
}
/* pos is at header end, adjust by actual packet length */
pkt->raw->pos += pkt->len;
(void) ngx_quic_skip_zero_padding(pkt->raw);
return ngx_quic_input(c, pkt->raw);
}
static ngx_int_t
ngx_quic_initial_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
{

View file

@ -23,6 +23,13 @@
#define NGX_QUIC_DEFAULT_ACK_DELAY_EXPONENT 3
#define NGX_QUIC_DEFAULT_MAX_ACK_DELAY 25
#define NGX_QUIC_RETRY_TIMEOUT 3000
#define NGX_QUIC_RETRY_LIFETIME 30000
#define NGX_QUIC_RETRY_BUFFER_SIZE 128
/* 1 flags + 4 version + 3 x (1 + 20) s/o/dcid + itag + token(44) */
#define NGX_QUIC_MAX_TOKEN_SIZE 32
/* sizeof(struct in6_addr) + sizeof(ngx_msec_t) up to AES-256 block size */
#define NGX_QUIC_HARDCODED_PTO 1000 /* 1s, TODO: collect */
#define NGX_QUIC_CC_MIN_INTERVAL 1000 /* 1s */
@ -49,9 +56,12 @@ typedef struct {
ngx_uint_t ack_delay_exponent;
ngx_uint_t disable_active_migration;
ngx_uint_t active_connection_id_limit;
ngx_str_t original_connection_id;
ngx_flag_t retry;
u_char token_key[32]; /* AES 256 */
/* TODO */
ngx_uint_t original_connection_id;
u_char stateless_reset_token[16];
void *preferred_address;
} ngx_quic_tp_t;

View file

@ -57,6 +57,8 @@ static ngx_int_t ngx_quic_create_long_packet(ngx_quic_header_t *pkt,
ngx_ssl_conn_t *ssl_conn, ngx_str_t *res);
static ngx_int_t ngx_quic_create_short_packet(ngx_quic_header_t *pkt,
ngx_ssl_conn_t *ssl_conn, ngx_str_t *res);
static ngx_int_t ngx_quic_create_retry_packet(ngx_quic_header_t *pkt,
ngx_str_t *res);
static ngx_int_t
@ -891,6 +893,53 @@ ngx_quic_create_short_packet(ngx_quic_header_t *pkt, ngx_ssl_conn_t *ssl_conn,
}
static ngx_int_t
ngx_quic_create_retry_packet(ngx_quic_header_t *pkt, ngx_str_t *res)
{
u_char *start;
ngx_str_t ad, itag;
ngx_quic_secret_t secret;
ngx_quic_ciphers_t ciphers;
/* 5.8. Retry Packet Integrity */
static u_char key[16] =
"\x4d\x32\xec\xdb\x2a\x21\x33\xc8"
"\x41\xe4\x04\x3d\xf2\x7d\x44\x30";
static u_char nonce[12] =
"\x4d\x16\x11\xd0\x55\x13"
"\xa5\x52\xc5\x87\xd5\x75";
static ngx_str_t in = ngx_string("");
ad.data = res->data;
ad.len = ngx_quic_create_retry_itag(pkt, ad.data, &start);
itag.data = ad.data + ad.len;
#ifdef NGX_QUIC_DEBUG_CRYPTO
ngx_quic_hexdump(pkt->log, "quic retry itag", ad.data, ad.len);
#endif
if (ngx_quic_ciphers(NULL, &ciphers, pkt->level) == NGX_ERROR) {
return NGX_ERROR;
}
secret.key.len = sizeof(key);
secret.key.data = key;
secret.iv.len = sizeof(nonce);
if (ngx_quic_tls_seal(ciphers.c, &secret, &itag, nonce, &in, &ad, pkt->log)
!= NGX_OK)
{
return NGX_ERROR;
}
res->len = itag.data + itag.len - start;
res->data = start;
return NGX_OK;
}
static uint64_t
ngx_quic_parse_pn(u_char **pos, ngx_int_t len, u_char *mask,
uint64_t *largest_pn)
@ -952,6 +1001,10 @@ ngx_quic_encrypt(ngx_quic_header_t *pkt, ngx_ssl_conn_t *ssl_conn,
return ngx_quic_create_short_packet(pkt, ssl_conn, res);
}
if (ngx_quic_pkt_retry(pkt->flags)) {
return ngx_quic_create_retry_packet(pkt, res);
}
return ngx_quic_create_long_packet(pkt, ssl_conn, res);
}

View file

@ -385,6 +385,35 @@ ngx_quic_create_short_header(ngx_quic_header_t *pkt, u_char *out,
}
size_t
ngx_quic_create_retry_itag(ngx_quic_header_t *pkt, u_char *out,
u_char **start)
{
u_char *p;
p = out;
*p++ = pkt->odcid.len;
p = ngx_cpymem(p, pkt->odcid.data, pkt->odcid.len);
*start = p;
*p++ = 0xff;
p = ngx_quic_write_uint32(p, NGX_QUIC_VERSION);
*p++ = pkt->dcid.len;
p = ngx_cpymem(p, pkt->dcid.data, pkt->dcid.len);
*p++ = pkt->scid.len;
p = ngx_cpymem(p, pkt->scid.data, pkt->scid.len);
p = ngx_cpymem(p, pkt->token.data, pkt->token.len);
return p - out;
}
ngx_int_t
ngx_quic_parse_short_header(ngx_quic_header_t *pkt, ngx_str_t *dcid)
{
@ -1553,6 +1582,12 @@ ngx_quic_create_transport_params(u_char *pos, u_char *end, ngx_quic_tp_t *tp)
len += ngx_quic_tp_len(NGX_QUIC_TP_MAX_IDLE_TIMEOUT,
tp->max_idle_timeout);
if (tp->retry) {
len += ngx_quic_varint_len(NGX_QUIC_TP_ORIGINAL_CONNECTION_ID);
len += ngx_quic_varint_len(tp->original_connection_id.len);
len += tp->original_connection_id.len;
}
if (pos == NULL) {
return len;
}
@ -1581,6 +1616,13 @@ ngx_quic_create_transport_params(u_char *pos, u_char *end, ngx_quic_tp_t *tp)
ngx_quic_tp_vint(NGX_QUIC_TP_MAX_IDLE_TIMEOUT,
tp->max_idle_timeout);
if (tp->retry) {
ngx_quic_build_int(&p, NGX_QUIC_TP_ORIGINAL_CONNECTION_ID);
ngx_quic_build_int(&p, tp->original_connection_id.len);
p = ngx_cpymem(p, tp->original_connection_id.data,
tp->original_connection_id.len);
}
return p - pos;
}

View file

@ -280,6 +280,7 @@ typedef struct {
size_t len;
/* cleartext fields */
ngx_str_t odcid; /* retry packet tag */
ngx_str_t dcid;
ngx_str_t scid;
uint64_t pn;
@ -303,6 +304,9 @@ ngx_int_t ngx_quic_parse_short_header(ngx_quic_header_t *pkt,
size_t ngx_quic_create_short_header(ngx_quic_header_t *pkt, u_char *out,
size_t pkt_len, u_char **pnp);
size_t ngx_quic_create_retry_itag(ngx_quic_header_t *pkt, u_char *out,
u_char **start);
ngx_int_t ngx_quic_parse_initial_header(ngx_quic_header_t *pkt);
ngx_int_t ngx_quic_parse_handshake_header(ngx_quic_header_t *pkt);

View file

@ -111,6 +111,13 @@ static ngx_command_t ngx_http_v3_commands[] = {
offsetof(ngx_http_v3_srv_conf_t, quic.active_connection_id_limit),
&ngx_http_v3_active_connection_id_limit_bounds },
{ ngx_string("quic_retry"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_SRV_CONF_OFFSET,
offsetof(ngx_http_v3_srv_conf_t, quic.retry),
NULL },
ngx_null_command
};
@ -257,6 +264,8 @@ ngx_http_v3_create_srv_conf(ngx_conf_t *cf)
v3cf->quic.disable_active_migration = NGX_CONF_UNSET_UINT;
v3cf->quic.active_connection_id_limit = NGX_CONF_UNSET_UINT;
v3cf->quic.retry = NGX_CONF_UNSET;
return v3cf;
}
@ -310,6 +319,15 @@ ngx_http_v3_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
ngx_conf_merge_uint_value(conf->quic.active_connection_id_limit,
prev->quic.active_connection_id_limit, 2);
ngx_conf_merge_value(conf->quic.retry, prev->quic.retry, 0);
if (conf->quic.retry) {
if (RAND_bytes(conf->quic.token_key, sizeof(conf->quic.token_key)) <= 0) {
return NGX_CONF_ERROR;
}
}
return NGX_CONF_OK;
}