Skip to content

Commit

Permalink
quic: stateless reset generate strategy
Browse files Browse the repository at this point in the history
Generate stateless reset token cryptographically

Fixes: nodejs#62
  • Loading branch information
jasnell committed Dec 5, 2019
1 parent 08bb8c9 commit 089e9dc
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 36 deletions.
4 changes: 4 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1671,6 +1671,10 @@ TBD

TBD

<a id="ERR_QUICSOCKET_INVALID_STATELESS_RESET_SECRET_LENGTH"></a>
### ERR_QUICSOCKET_INVALID_STATELESS_RESET_SECRET_LENGTH
TBD

<a id="ERR_QUICSOCKET_LISTENING"></a>
### ERR_QUICSOCKET_LISTENING

Expand Down
9 changes: 9 additions & 0 deletions doc/api/quic.md
Original file line number Diff line number Diff line change
Expand Up @@ -1596,6 +1596,15 @@ Changing TTL values is typically done for network probes or when multicasting.
The argument to `socket.setTTL()` is a number of hops between `1` and `255`.
The default on most systems is `64` but can vary.

#### quicsocket.statelessResetCount
<!-- YAML
added: REPLACEME
-->

* Type: {BigInt}

A `BigInt` that represents the number of stateless resets that have been sent.

#### quicsocket.unref();
<!-- YAML
added: REPLACEME
Expand Down
3 changes: 3 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,9 @@ E('ERR_QUICSOCKET_CLOSING',
'Cannot call %s while a QuicSocket is closing', Error);
E('ERR_QUICSOCKET_DESTROYED',
'Cannot call %s after a QuicSocket has been destroyed', Error);
E('ERR_QUICSOCKET_INVALID_STATELESS_RESET_SECRET_LENGTH',
'The stateResetToken must be exactly 16-bytes in length',
Error);
E('ERR_QUICSOCKET_LISTENING',
'This QuicSocket is already listening', Error);
E('ERR_QUICSOCKET_UNBOUND',
Expand Down
12 changes: 11 additions & 1 deletion lib/internal/quic/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ const {
IDX_QUIC_SOCKET_STATS_PACKETS_SENT,
IDX_QUIC_SOCKET_STATS_SERVER_SESSIONS,
IDX_QUIC_SOCKET_STATS_CLIENT_SESSIONS,
IDX_QUIC_SOCKET_STATS_STATELESS_RESET_COUNT,
ERR_INVALID_REMOTE_TRANSPORT_PARAMS,
ERR_INVALID_TLS_SESSION_TICKET,
NGTCP2_PATH_VALIDATION_RESULT_FAILURE,
Expand Down Expand Up @@ -809,6 +810,9 @@ class QuicSocket extends EventEmitter {

// Whether qlog should be enabled for sessions
qlog,

// Stateless reset token secret (16 byte buffer)
statelessResetSecret
} = validateQuicSocketOptions(options);
super();
const socketOptions =
Expand All @@ -827,7 +831,8 @@ class QuicSocket extends EventEmitter {
socketOptions,
retryTokenTimeout,
maxConnectionsPerHost,
qlog);
qlog,
statelessResetSecret);
udpHandle.quicSocket = handle;
handle[owner_symbol] = this;
this[async_id_symbol] = handle.getAsyncId();
Expand Down Expand Up @@ -1397,6 +1402,11 @@ class QuicSocket extends EventEmitter {
return stats[IDX_QUIC_SOCKET_STATS_CLIENT_SESSIONS];
}

get statelessResetCount() {
const stats = this.#stats || this[kHandle].stats;
return stats[IDX_QUIC_SOCKET_STATS_STATELESS_RESET_COUNT];
}

setDiagnosticPacketLoss(options) {
if (this.#state === kSocketDestroyed)
throw new ERR_QUICSOCKET_DESTROYED('setDiagnosticPacketLoss');
Expand Down
14 changes: 14 additions & 0 deletions lib/internal/quic/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
ERR_INVALID_ARG_VALUE,
ERR_OUT_OF_RANGE,
ERR_QUICSESSION_INVALID_DCID,
ERR_QUICSOCKET_INVALID_STATELESS_RESET_SECRET_LENGTH,
},
} = require('internal/errors');

Expand Down Expand Up @@ -369,6 +370,7 @@ function validateQuicSocketOptions(options) {
validateAddressLRU = false,
retryTokenTimeout = DEFAULT_RETRYTOKEN_EXPIRATION,
qlog = false,
statelessResetSecret,
} = { ...options };
validateBindOptions(port, address);
if (typeof type !== 'string')
Expand Down Expand Up @@ -411,6 +413,17 @@ function validateQuicSocketOptions(options) {
throw new ERR_INVALID_ARG_TYPE('options.qlog', 'boolean', qlog);
}

if (statelessResetSecret !== undefined) {
if (!isArrayBufferView(statelessResetSecret)) {
throw new ERR_INVALID_ARG_TYPE(
'options.statelessResetSecret',
['string', 'Buffer', 'TypedArray', 'DataView'],
statelessResetSecret);
}
if (statelessResetSecret.length !== 16)
throw new ERR_QUICSOCKET_INVALID_STATELESS_RESET_SECRET_LENGTH();
}

return {
address,
autoClose,
Expand All @@ -426,6 +439,7 @@ function validateQuicSocketOptions(options) {
validateAddress: validateAddress || validateAddressLRU,
validateAddressLRU,
qlog,
statelessResetSecret,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/node_quic.cc
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ void Initialize(Local<Object> target,
NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SOCKET_STATS_PACKETS_SENT);
NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SOCKET_STATS_SERVER_SESSIONS);
NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SOCKET_STATS_CLIENT_SESSIONS);
NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SOCKET_STATS_STATELESS_RESET_COUNT);

NODE_DEFINE_CONSTANT(constants, IDX_HTTP3_QPACK_MAX_TABLE_CAPACITY);
NODE_DEFINE_CONSTANT(constants, IDX_HTTP3_QPACK_BLOCKED_STREAMS);
Expand Down
17 changes: 17 additions & 0 deletions src/node_quic_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,23 @@ void GenerateRandData(uint8_t* buf, size_t len) {
}
} // namespace

bool GenerateResetToken(
uint8_t* token,
uint8_t* secret,
size_t secretlen,
const ngtcp2_cid* cid) {
ngtcp2_crypto_ctx ctx;
ngtcp2_crypto_ctx_initial(&ctx);
return NGTCP2_OK(ngtcp2_crypto_hkdf_expand(
token,
NGTCP2_STATELESS_RESET_TOKENLEN,
&ctx.md,
secret,
secretlen,
cid->data,
cid->datalen));
}

// The Retry Token is an encrypted token that is sent to the client
// by the server as part of the path validation flow. The plaintext
// format within the token is opaque and only meaningful the server.
Expand Down
6 changes: 6 additions & 0 deletions src/node_quic_crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ bool UpdateKey(
std::vector<uint8_t>* current_rx_secret,
std::vector<uint8_t>* current_tx_secret);

bool GenerateResetToken(
uint8_t* token,
uint8_t* secret,
size_t secretlen,
const ngtcp2_cid* cid);

bool GenerateRetryToken(
uint8_t* token,
size_t* tokenlen,
Expand Down
32 changes: 28 additions & 4 deletions src/node_quic_session.cc
Original file line number Diff line number Diff line change
Expand Up @@ -678,20 +678,28 @@ void JSQuicSessionListener::OnQLog(const uint8_t* data, size_t len) {
&str);
}

// Generates and associates a new connection ID for this QuicSession.
// Generates a new connection ID for this QuicSession.
// ngtcp2 will call this multiple times at the start of a new connection
// in order to build a pool of available CIDs.
void RandomConnectionIDStrategy::GetNewConnectionID(
QuicSession* session,
ngtcp2_cid* cid,
uint8_t* token,
size_t cidlen) {
cid->datalen = cidlen;
// cidlen shouldn't ever be zero here but just in case that
// behavior changes in ngtcp2 in the future...
if (cidlen > 0)
EntropySource(cid->data, cidlen);
EntropySource(token, NGTCP2_STATELESS_RESET_TOKENLEN);
}

void CryptoStatelessResetTokenStrategy::GetNewStatelessToken(
QuicSession* session,
ngtcp2_cid* cid,
uint8_t* token,
size_t tokenlen) {
std::array<uint8_t, NGTCP2_STATELESS_RESET_TOKENLEN>* secret =
session->Socket()->GetSessionResetSecret();
CHECK(GenerateResetToken(token, secret->data(), secret->size(), cid));
}

// Check required capabilities were not excluded from the OpenSSL build:
Expand Down Expand Up @@ -1260,6 +1268,7 @@ QuicSession::QuicSession(
reinterpret_cast<double*>(&recovery_stats_)) {
PushListener(&default_listener_);
SetConnectionIDStrategory(&default_connection_id_strategy_);
SetStatelessResetTokenStrategy(&default_stateless_reset_strategy_);
crypto_context_.reset(new QuicCryptoContext(this, ctx, side, options));
application_.reset(SelectApplication(this));
if (rcid != nullptr)
Expand Down Expand Up @@ -1634,6 +1643,12 @@ void QuicSession::SetConnectionIDStrategory(ConnectionIDStrategy* strategy) {
connection_id_strategy_ = strategy;
}

void QuicSession::SetStatelessResetTokenStrategy(
StatelessResetTokenStrategy* strategy) {
CHECK_NOT_NULL(strategy);
stateless_reset_strategy_ = strategy;
}

// Generates and associates a new connection ID for this QuicSession.
// ngtcp2 will call this multiple times at the start of a new connection
// in order to build a pool of available CIDs.
Expand All @@ -1643,7 +1658,16 @@ int QuicSession::GetNewConnectionID(
size_t cidlen) {
DCHECK(!IsFlagSet(QUICSESSION_FLAG_DESTROYED));
CHECK_NOT_NULL(connection_id_strategy_);
connection_id_strategy_->GetNewConnectionID(this, cid, token, cidlen);
connection_id_strategy_->GetNewConnectionID(
this,
cid,
cidlen);
stateless_reset_strategy_->GetNewStatelessToken(
this,
cid,
token,
NGTCP2_STATELESS_RESET_TOKENLEN);

AssociateCID(cid);
return 0;
}
Expand Down
24 changes: 22 additions & 2 deletions src/node_quic_session.h
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,6 @@ class ConnectionIDStrategy {
virtual void GetNewConnectionID(
QuicSession* session,
ngtcp2_cid* cid,
uint8_t* token,
size_t cidlen) = 0;
};

Expand All @@ -304,10 +303,27 @@ class RandomConnectionIDStrategy : public ConnectionIDStrategy {
void GetNewConnectionID(
QuicSession* session,
ngtcp2_cid* cid,
uint8_t* token,
size_t cidlen) override;
};

class StatelessResetTokenStrategy {
public:
virtual void GetNewStatelessToken(
QuicSession* session,
ngtcp2_cid* cid,
uint8_t* token,
size_t tokenlen) = 0;
};

class CryptoStatelessResetTokenStrategy : public StatelessResetTokenStrategy {
public:
void GetNewStatelessToken(
QuicSession* session,
ngtcp2_cid* cid,
uint8_t* token,
size_t tokenlen) override;
};

// The QuicCryptoContext class encapsulates all of the crypto/TLS
// handshake details on behalf of a QuicSession.
class QuicCryptoContext : public MemoryRetainer {
Expand Down Expand Up @@ -935,6 +951,7 @@ class QuicSession : public AsyncWrap,
void RemoveListener(QuicSessionListener* listener);

void SetConnectionIDStrategory(ConnectionIDStrategy* strategy);
void SetStatelessResetTokenStrategy(StatelessResetTokenStrategy* strategy);

// Report that the stream data is flow control blocked
void StreamDataBlocked(int64_t stream_id);
Expand Down Expand Up @@ -1298,6 +1315,9 @@ class QuicSession : public AsyncWrap,
ConnectionIDStrategy* connection_id_strategy_ = nullptr;
RandomConnectionIDStrategy default_connection_id_strategy_;

StatelessResetTokenStrategy* stateless_reset_strategy_ = nullptr;
CryptoStatelessResetTokenStrategy default_stateless_reset_strategy_;

QuicSessionListener* listener_ = nullptr;
JSQuicSessionListener default_listener_;

Expand Down
Loading

0 comments on commit 089e9dc

Please sign in to comment.