Skip to content
83 changes: 83 additions & 0 deletions api/unstable/async_offload.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

#pragma once

#include <s2n.h>

/**
* @file async_offload.h
*
* The following APIs enable applications to offload expensive handshake operations that do not require user input.
* Use s2n_config_set_async_offload_callback() to configure the async callback and the set of operations to offload.
*
* `s2n_negotiate()` will throw an `S2N_ERR_T_BLOCKED` error if the handshake is blocked by the offloading callback.
* s2n_async_op_perform() must be invoked to unblock the handshake.
*/

/**
* Opaque struct for the async offloading operation
*/
struct s2n_async_op;

/**
* The type of operations supported by the async offloading callback. Each type is represented by a different bit.
*
* WARNING: S2N_ASYNC_ALLOW_ALL will automatically opt in to all the new types added in the future.
*/
typedef enum {
S2N_ASYNC_OP_NONE = 0,
S2N_ASYNC_PKEY_VERIFY = 0x01,
S2N_ASYNC_ALLOW_ALL = 0x7FFFFFFF,
} s2n_async_op_type;

/**
* The callback function invoked every time an allowed async operation is encountered during the handshake.
*
* To perform an operation asynchronously, the following condiditions must be satisfied:
* 1) This op type must be included in the allow list;
* 2) Async offloading callback returns success and s2n_async_op_perform() is invoked outside the callback.
*
* If s2n_async_op_perform() is invoked inside the callback, it is equivalent to the synchronous use case.
*
* `op` is owned by s2n-tls and will be freed along with s2n_connection.
*
* @param conn Connection which triggered the async offloading callback
* @param op An opaque object representing the async operation
* @param ctx Application data provided to the callback via s2n_config_set_async_offload_callback()
*/
typedef int (*s2n_async_offload_cb)(struct s2n_connection *conn, struct s2n_async_op *op, void *ctx);

/**
* Sets up the async callback to offload handshake operations configured via the allow_list.
*
* The default allow list for s2n_config is S2N_ASYNC_OP_NONE.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no default anymore right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async_offload_allow_list is a uint32 in s2n_config. It should be zero by default. I also have a check in the unit test:

61    EXPECT_EQUAL(test_config->async_offload_allow_list, S2N_ASYNC_OP_NONE);

*
* @param config Config to set the callback
* @param fn The function that should be called for each allowed async operation
* @param allow_list A bit representation of allowed operations (Bit-OR of all the allowd s2n_async_op_type values)
* @param ctx Optional application data passed to the callback
*/
S2N_API extern int s2n_config_set_async_offload_callback(struct s2n_config *config, s2n_async_offload_cb fn,
uint32_t allow_list, void *ctx);

/**
* Performs the operation triggered by the async offloading callback.
*
* op_perform() can only be called once for each triggered operation.
*
* @param op An opaque object representing the async operation
*/
S2N_API extern int s2n_async_op_perform(struct s2n_async_op *op);
1 change: 1 addition & 0 deletions bindings/rust/extended/s2n-tls-sys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ fips = ["aws-lc-rs/fips"]
pq = []
internal = []
stacktrace = []
unstable-async_offload = []
unstable-cert_authorities = []
unstable-cleanup = []
unstable-crl = []
Expand Down
2 changes: 1 addition & 1 deletion error/s2n_errno.c
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ static const char *no_such_error = "Internal s2n error";
ERR_ENTRY(S2N_ERR_RESIZE_STATIC_BLOB, "Cannot resize a static blob") \
ERR_ENTRY(S2N_ERR_RECORD_LENGTH_TOO_LARGE, "Record length exceeds protocol version maximum") \
ERR_ENTRY(S2N_ERR_SET_DUPLICATE_VALUE, "Set already contains the provided value") \
ERR_ENTRY(S2N_ERR_ASYNC_CALLBACK_FAILED, "Callback associated with async private keys function has failed") \
ERR_ENTRY(S2N_ERR_ASYNC_CALLBACK_FAILED, "Callback associated with async function has failed") \
ERR_ENTRY(S2N_ERR_ASYNC_MORE_THAN_ONE, "Only one asynchronous operation can be in-progress at the same time") \
ERR_ENTRY(S2N_ERR_NO_ALERT, "No Alert present") \
ERR_ENTRY(S2N_ERR_SERVER_MODE, "Operation not allowed in server mode") \
Expand Down
245 changes: 245 additions & 0 deletions tests/unit/s2n_async_offload_cb_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

#include "api/s2n.h"
#include "error/s2n_errno.h"
#include "s2n_test.h"
#include "testlib/s2n_testlib.h"
#include "tls/s2n_async_offload.h"
#include "tls/s2n_cipher_suites.h"
#include "tls/s2n_connection.h"
#include "tls/s2n_security_policies.h"
#include "utils/s2n_safety.h"

struct s2n_async_offload_cb_test {
unsigned invoke_perform : 1;
int result;

int invoked_count;
struct s2n_async_op *op;
};

int s2n_async_offload_test_callback(struct s2n_connection *conn, struct s2n_async_op *op, void *ctx)
{
EXPECT_NOT_NULL(op);

struct s2n_async_offload_cb_test *data = (struct s2n_async_offload_cb_test *) ctx;
data->invoked_count += 1;
data->op = op;

if (data->invoke_perform) {
EXPECT_SUCCESS(s2n_async_op_perform(op));
}

return data->result;
}

static int test_handshake_async(struct s2n_connection *server_conn, struct s2n_connection *client_conn,
struct s2n_async_offload_cb_test *data)
{
/* Test retry while handshake is blocked */
for (int i = 0; i < 3; i++) {
EXPECT_FAILURE_WITH_ERRNO(s2n_negotiate_test_server_and_client(server_conn, client_conn),
S2N_ERR_ASYNC_BLOCKED);
}

while (true) {
int ret_val = s2n_negotiate_test_server_and_client(server_conn, client_conn);

if (ret_val == 0) {
break;
} else if (s2n_errno == S2N_ERR_ASYNC_BLOCKED) {
EXPECT_SUCCESS(s2n_async_op_perform(data->op));
/* Each operation can only be performed once. */
EXPECT_FAILURE_WITH_ERRNO(s2n_async_op_perform(data->op), S2N_ERR_INVALID_STATE);
} else {
return s2n_errno;
}
}

return S2N_SUCCESS;
}

int main(int argc, char *argv[])
{
BEGIN_TEST();

/* Safety Check */
{
struct s2n_async_offload_cb_test test_data = { 0 };
EXPECT_FAILURE_WITH_ERRNO(s2n_config_set_async_offload_callback(NULL, s2n_async_offload_test_callback, S2N_ASYNC_ALLOW_ALL, &test_data),
S2N_ERR_NULL);

DEFER_CLEANUP(struct s2n_config *test_config = s2n_config_new(), s2n_config_ptr_free);
EXPECT_NOT_NULL(test_config);
EXPECT_EQUAL(test_config->async_offload_allow_list, S2N_ASYNC_OP_NONE);
EXPECT_FAILURE_WITH_ERRNO(s2n_config_set_async_offload_callback(test_config, NULL, S2N_ASYNC_ALLOW_ALL, &test_data),
S2N_ERR_NULL);

EXPECT_SUCCESS(s2n_config_set_async_offload_callback(test_config, s2n_async_offload_test_callback,
S2N_ASYNC_PKEY_VERIFY, &test_data));
EXPECT_TRUE(s2n_async_is_op_in_allow_list(test_config, S2N_ASYNC_PKEY_VERIFY));

EXPECT_FAILURE_WITH_ERRNO(s2n_async_op_perform(NULL), S2N_ERR_NULL);
struct s2n_async_op test_op = { 0 };
EXPECT_FAILURE_WITH_ERRNO(s2n_async_op_perform(&test_op), S2N_ERR_INVALID_STATE);
}

DEFER_CLEANUP(struct s2n_cert_chain_and_key *chain_and_key = NULL, s2n_cert_chain_and_key_ptr_free);
EXPECT_SUCCESS(s2n_test_cert_chain_and_key_new(&chain_and_key,
S2N_DEFAULT_TEST_CERT_CHAIN, S2N_DEFAULT_TEST_PRIVATE_KEY));

/* clang-format off */
struct {
s2n_async_op_type allow_list;
int cb_return;
int cb_invoked;
s2n_error expected_error;
} sync_tests[] = {
/* Default option: no op type is allowed. */
{
.allow_list = S2N_ASYNC_OP_NONE,
.cb_return = S2N_SUCCESS,
.cb_invoked = 0,
.expected_error = S2N_ERR_OK,
},
/* Test a random value that has not been used by any op type. */
/* Changing return value does not fail the handshake because the callback is not invoked. */
{
.allow_list = 0x1000,
.cb_return = S2N_FAILURE,
.cb_invoked = 0,
.expected_error = S2N_ERR_OK,
},
/* ASYNC_PKEY_VERIFY is allowed. Client auth is enabled for all the sync tests. */
/* In this case, a successfule handshake performs pkey_verify() twice. */
{
.allow_list = S2N_ASYNC_PKEY_VERIFY,
.cb_return = S2N_SUCCESS,
.cb_invoked = 2,
.expected_error = S2N_ERR_OK,
},
/* Any op type is allowed. Handshake failed because the callback failed in the first attempt. */
{
.allow_list = S2N_ASYNC_ALLOW_ALL,
.cb_return = S2N_FAILURE,
.cb_invoked = 1,
.expected_error = S2N_ERR_CANCELLED,
},
};
/* clang-format on */

/* Test with both TLS 1.2 and TLS 1.3 policies */
const char *versions[] = { "20240501", "default_tls13" };

/* Sync Test: 1) op type is not allowed, or 2) op_perform() invoked in the callback. */
for (int test_idx = 0; test_idx < s2n_array_len(sync_tests); test_idx++) {
for (int version_idx = 0; version_idx < s2n_array_len(versions); version_idx++) {
DEFER_CLEANUP(struct s2n_config *config = s2n_config_new(), s2n_config_ptr_free);
EXPECT_NOT_NULL(config);
EXPECT_SUCCESS(s2n_config_add_cert_chain_and_key_to_store(config, chain_and_key));
EXPECT_SUCCESS(s2n_config_set_verification_ca_location(config, S2N_DEFAULT_TEST_CERT_CHAIN, NULL));
EXPECT_SUCCESS(s2n_config_set_cipher_preferences(config, versions[version_idx]));
EXPECT_SUCCESS(s2n_config_set_client_auth_type(config, S2N_CERT_AUTH_REQUIRED));

struct s2n_async_offload_cb_test data = { .invoke_perform = true, .result = sync_tests[test_idx].cb_return };
EXPECT_SUCCESS(s2n_config_set_async_offload_callback(config, s2n_async_offload_test_callback,
sync_tests[test_idx].allow_list, &data));

DEFER_CLEANUP(struct s2n_connection *server_conn = s2n_connection_new(S2N_SERVER), s2n_connection_ptr_free);
EXPECT_NOT_NULL(server_conn);
EXPECT_SUCCESS(s2n_connection_set_config(server_conn, config));

DEFER_CLEANUP(struct s2n_connection *client_conn = s2n_connection_new(S2N_CLIENT), s2n_connection_ptr_free);
EXPECT_NOT_NULL(client_conn);
EXPECT_SUCCESS(s2n_connection_set_config(client_conn, config));
EXPECT_SUCCESS(s2n_connection_set_blinding(client_conn, S2N_SELF_SERVICE_BLINDING));
EXPECT_SUCCESS(s2n_set_server_name(client_conn, "localhost"));

DEFER_CLEANUP(struct s2n_test_io_pair io_pair = { 0 }, s2n_io_pair_close);
EXPECT_SUCCESS(s2n_io_pair_init_non_blocking(&io_pair));
EXPECT_SUCCESS(s2n_connection_set_io_pair(client_conn, &io_pair));
EXPECT_SUCCESS(s2n_connection_set_io_pair(server_conn, &io_pair));

s2n_error expected_error = sync_tests[test_idx].expected_error;
if (expected_error == S2N_ERR_OK) {
EXPECT_SUCCESS(s2n_negotiate_test_server_and_client(server_conn, client_conn));
} else {
EXPECT_FAILURE_WITH_ERRNO(s2n_negotiate_test_server_and_client(server_conn, client_conn), expected_error);
}

EXPECT_EQUAL(data.invoked_count, sync_tests[test_idx].cb_invoked);
}
}

/* clang-format off */
struct {
s2n_async_op_type allow_list;
bool client_auth;
int cb_invoked;
} async_tests[] = {
/* Client auth is enabled. pkey_verify() is performed by both server side and client side. */
{
.allow_list = S2N_ASYNC_PKEY_VERIFY,
.client_auth = true,
.cb_invoked = 2,
},
/* Client auth is not enabled. pkey_verify() is performed only by client side. */
{
.allow_list = S2N_ASYNC_ALLOW_ALL,
.client_auth = false,
.cb_invoked = 1,
},
};
/* clang-format on */

/* Async verify test: ASYNC_PKEY_VERIFY is allowed, and op_perform() invoked outside the callback. */
for (int test_idx = 0; test_idx < s2n_array_len(async_tests); test_idx++) {
for (int version_idx = 0; version_idx < s2n_array_len(versions); version_idx++) {
DEFER_CLEANUP(struct s2n_config *config = s2n_config_new(), s2n_config_ptr_free);
EXPECT_NOT_NULL(config);
EXPECT_SUCCESS(s2n_config_add_cert_chain_and_key_to_store(config, chain_and_key));
EXPECT_SUCCESS(s2n_config_set_verification_ca_location(config, S2N_DEFAULT_TEST_CERT_CHAIN, NULL));
EXPECT_SUCCESS(s2n_config_set_cipher_preferences(config, versions[version_idx]));
if (async_tests[test_idx].client_auth) {
EXPECT_SUCCESS(s2n_config_set_client_auth_type(config, S2N_CERT_AUTH_REQUIRED));
}

struct s2n_async_offload_cb_test data = { .invoke_perform = false, .result = S2N_SUCCESS };
EXPECT_SUCCESS(s2n_config_set_async_offload_callback(config, s2n_async_offload_test_callback,
async_tests[test_idx].allow_list, &data));

DEFER_CLEANUP(struct s2n_connection *server_conn = s2n_connection_new(S2N_SERVER), s2n_connection_ptr_free);
EXPECT_NOT_NULL(server_conn);
EXPECT_SUCCESS(s2n_connection_set_config(server_conn, config));

DEFER_CLEANUP(struct s2n_connection *client_conn = s2n_connection_new(S2N_CLIENT), s2n_connection_ptr_free);
EXPECT_NOT_NULL(client_conn);
EXPECT_SUCCESS(s2n_connection_set_config(client_conn, config));
EXPECT_SUCCESS(s2n_connection_set_blinding(client_conn, S2N_SELF_SERVICE_BLINDING));
EXPECT_SUCCESS(s2n_set_server_name(client_conn, "localhost"));

DEFER_CLEANUP(struct s2n_test_io_pair io_pair = { 0 }, s2n_io_pair_close);
EXPECT_SUCCESS(s2n_io_pair_init_non_blocking(&io_pair));
EXPECT_SUCCESS(s2n_connection_set_io_pair(client_conn, &io_pair));
EXPECT_SUCCESS(s2n_connection_set_io_pair(server_conn, &io_pair));

EXPECT_SUCCESS(test_handshake_async(server_conn, client_conn, &data));
EXPECT_EQUAL(data.invoked_count, async_tests[test_idx].cb_invoked);
}
}

END_TEST();
}
2 changes: 1 addition & 1 deletion tests/unit/s2n_connection_size_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ int main(int argc, char **argv)
}

/* Carefully consider any increases to this number. */
const uint16_t max_connection_size = 4350;
const uint16_t max_connection_size = 4360;
const uint16_t min_connection_size = max_connection_size * 0.9;

size_t connection_size = sizeof(struct s2n_connection);
Expand Down
Loading
Loading