diff --git a/api/s2n.h b/api/s2n.h index 0ba344885aa..ad55db3e559 100644 --- a/api/s2n.h +++ b/api/s2n.h @@ -3186,6 +3186,38 @@ S2N_API extern int s2n_connection_client_cert_used(struct s2n_connection *conn); */ S2N_API extern const char *s2n_connection_get_cipher(struct s2n_connection *conn); +/** + * A metric to determine whether or not the server found a certificate that matched + * the client's SNI extension. + * + * S2N_SNI_NONE: Client did not send the SNI extension. + * S2N_SNI_EXACT_MATCH: Server had a certificate that matched the client's SNI extension. + * S2N_SNI_WILDCARD_MATCH: Server had a certificate with a domain name containing a wildcard character + * that could be matched to the client's SNI extension. + * S2N_SNI_NO_MATCH: Server did not have a certificate that could be matched to the client's + * SNI extension. + */ +typedef enum { + S2N_SNI_NONE = 1, + S2N_SNI_EXACT_MATCH, + S2N_SNI_WILDCARD_MATCH, + S2N_SNI_NO_MATCH, +} s2n_cert_sni_match; + +/** + * A function that provides insight into whether or not the server was able to send a certificate that + * partially or completely matched the client's SNI extension. + * + * @note This function can be used as a metric in a failed connection as long as the failure + * occurs after certificate selection. + * + * @param conn A pointer to the connection + * @param cert_match An enum indicating whether or not the server found a certificate + * that matched the client's SNI extension. + * @returns S2N_SUCCESS on success. S2N_FAILURE on failure. + */ +S2N_API extern int s2n_connection_get_certificate_match(struct s2n_connection *conn, s2n_cert_sni_match *match_status); + /** * Provides access to the TLS master secret. * diff --git a/tests/testlib/s2n_testlib.h b/tests/testlib/s2n_testlib.h index 5f91d2302dd..d7d4ca9c3f1 100644 --- a/tests/testlib/s2n_testlib.h +++ b/tests/testlib/s2n_testlib.h @@ -120,6 +120,7 @@ S2N_RESULT s2n_connection_set_test_master_secret(struct s2n_connection *conn, co #define S2N_RSA_2048_SHA256_NO_DNS_SANS_CERT "../pems/rsa_2048_sha256_no_dns_sans_cert.pem" #define S2N_RSA_2048_SHA256_WILDCARD_CERT "../pems/rsa_2048_sha256_wildcard_cert.pem" +#define S2N_RSA_2048_SHA256_WILDCARD_KEY "../pems/rsa_2048_sha256_wildcard_key.pem" #define S2N_RSA_2048_SHA256_URI_SANS_CERT "../pems/rsa_2048_sha256_uri_sans_cert.pem" diff --git a/tests/unit/s2n_wildcard_hostname_test.c b/tests/unit/s2n_wildcard_hostname_test.c index 0d059053c79..1ff5254f7b0 100644 --- a/tests/unit/s2n_wildcard_hostname_test.c +++ b/tests/unit/s2n_wildcard_hostname_test.c @@ -41,7 +41,6 @@ struct wildcardify_test_case wildcardify_test_cases[] = { int main(int argc, char **argv) { BEGIN_TEST(); - EXPECT_SUCCESS(s2n_disable_tls13_in_test()); const int num_wildcardify_tests = s2n_array_len(wildcardify_test_cases); for (size_t i = 0; i < num_wildcardify_tests; i++) { @@ -69,5 +68,142 @@ int main(int argc, char **argv) } } + /* s2n_connection_get_certificate_match */ + { + /* Safety checks */ + { + s2n_cert_sni_match match_status = 0; + struct s2n_connection *conn = NULL; + EXPECT_FAILURE_WITH_ERRNO(s2n_connection_get_certificate_match(NULL, &match_status), + S2N_ERR_INVALID_ARGUMENT); + EXPECT_FAILURE_WITH_ERRNO(s2n_connection_get_certificate_match(conn, NULL), + S2N_ERR_INVALID_ARGUMENT); + + /* This API does not work on a client connection */ + DEFER_CLEANUP(struct s2n_connection *client_conn = s2n_connection_new(S2N_CLIENT), + s2n_connection_ptr_free); + EXPECT_NOT_NULL(client_conn); + EXPECT_FAILURE_WITH_ERRNO(s2n_connection_get_certificate_match(client_conn, &match_status), + S2N_ERR_CLIENT_MODE); + + /* This API will not work if a certificate isn't selected yet. */ + DEFER_CLEANUP(struct s2n_connection *server_conn = s2n_connection_new(S2N_SERVER), + s2n_connection_ptr_free); + EXPECT_NOT_NULL(server_conn); + EXPECT_FAILURE_WITH_ERRNO(s2n_connection_get_certificate_match(server_conn, &match_status), + S2N_ERR_NO_CERT_FOUND); + } + + const struct { + const char *cert_path; + const char *key_path; + const char *server_name; + s2n_cert_sni_match expected_match_status; + } test_cases[] = { + /* Client does not send an SNI extension */ + { + .cert_path = S2N_DEFAULT_TEST_CERT_CHAIN, + .key_path = S2N_DEFAULT_TEST_PRIVATE_KEY, + .server_name = NULL, + .expected_match_status = S2N_SNI_NONE, + }, + /* Server has a certificate that matches the client's SNI extension */ + { + .cert_path = S2N_DEFAULT_TEST_CERT_CHAIN, + .key_path = S2N_DEFAULT_TEST_PRIVATE_KEY, + .server_name = "localhost", + .expected_match_status = S2N_SNI_EXACT_MATCH, + }, + /* Server has a certificate with a domain name containing a wildcard character + * which can be matched to the client's SNI extension */ + { + .cert_path = S2N_RSA_2048_SHA256_WILDCARD_CERT, + .key_path = S2N_RSA_2048_SHA256_WILDCARD_KEY, + .server_name = "alligator.localhost", + .expected_match_status = S2N_SNI_WILDCARD_MATCH, + }, + /* Server does not have a certificate that can be matched to the client's + * SNI extension. */ + { + .cert_path = S2N_DEFAULT_TEST_CERT_CHAIN, + .key_path = S2N_DEFAULT_TEST_PRIVATE_KEY, + .server_name = "This cert name is unlikely to exist.", + .expected_match_status = S2N_SNI_NO_MATCH, + }, + }; + + for (size_t i = 0; i < s2n_array_len(test_cases); i++) { + DEFER_CLEANUP(struct s2n_connection *client_conn = s2n_connection_new(S2N_CLIENT), + s2n_connection_ptr_free); + EXPECT_NOT_NULL(client_conn); + + DEFER_CLEANUP(struct s2n_connection *server_conn = s2n_connection_new(S2N_SERVER), + s2n_connection_ptr_free); + EXPECT_NOT_NULL(server_conn); + + DEFER_CLEANUP(struct s2n_config *config = s2n_config_new(), s2n_config_ptr_free); + EXPECT_NOT_NULL(config); + 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, + test_cases[i].cert_path, test_cases[i].key_path)); + EXPECT_SUCCESS(s2n_config_disable_x509_verification(config)); + EXPECT_SUCCESS(s2n_config_add_cert_chain_and_key_to_store(config, chain_and_key)); + + EXPECT_SUCCESS(s2n_connection_set_config(server_conn, config)); + EXPECT_SUCCESS(s2n_connection_set_config(client_conn, config)); + + DEFER_CLEANUP(struct s2n_test_io_stuffer_pair io_pair = { 0 }, s2n_io_stuffer_pair_free); + EXPECT_OK(s2n_io_stuffer_pair_init(&io_pair)); + EXPECT_OK(s2n_connections_set_io_stuffer_pair(server_conn, client_conn, &io_pair)); + + const char *server_name = test_cases[i].server_name; + if (server_name) { + EXPECT_SUCCESS(s2n_set_server_name(client_conn, server_name)); + } + + EXPECT_SUCCESS(s2n_negotiate_test_server_and_client(server_conn, client_conn)); + + s2n_cert_sni_match match_status = 0; + EXPECT_SUCCESS(s2n_connection_get_certificate_match(server_conn, &match_status)); + EXPECT_EQUAL(match_status, test_cases[i].expected_match_status); + } + + /* Test that cert info can still be retrieved in the case of a failed handshake */ + { + DEFER_CLEANUP(struct s2n_connection *client_conn = s2n_connection_new(S2N_CLIENT), + s2n_connection_ptr_free); + EXPECT_NOT_NULL(client_conn); + + DEFER_CLEANUP(struct s2n_connection *server_conn = s2n_connection_new(S2N_SERVER), + s2n_connection_ptr_free); + EXPECT_NOT_NULL(server_conn); + + DEFER_CLEANUP(struct s2n_config *config = s2n_config_new(), s2n_config_ptr_free); + EXPECT_NOT_NULL(config); + 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)); + EXPECT_SUCCESS(s2n_config_disable_x509_verification(config)); + EXPECT_SUCCESS(s2n_config_add_cert_chain_and_key_to_store(config, chain_and_key)); + + EXPECT_SUCCESS(s2n_connection_set_config(server_conn, config)); + EXPECT_SUCCESS(s2n_connection_set_blinding(client_conn, S2N_SELF_SERVICE_BLINDING)); + + DEFER_CLEANUP(struct s2n_test_io_stuffer_pair io_pair = { 0 }, s2n_io_stuffer_pair_free); + EXPECT_OK(s2n_io_stuffer_pair_init(&io_pair)); + EXPECT_OK(s2n_connections_set_io_stuffer_pair(server_conn, client_conn, &io_pair)); + + EXPECT_SUCCESS(s2n_set_server_name(client_conn, "This cert name is unlikely to exist.")); + EXPECT_FAILURE_WITH_ERRNO(s2n_negotiate_test_server_and_client(server_conn, client_conn), + S2N_ERR_CERT_UNTRUSTED); + + s2n_cert_sni_match match_status = 0; + EXPECT_SUCCESS(s2n_connection_get_certificate_match(server_conn, &match_status)); + EXPECT_EQUAL(match_status, S2N_SNI_NO_MATCH); + } + } + END_TEST(); } diff --git a/tls/s2n_connection.c b/tls/s2n_connection.c index 0364a8ebf91..fcc737d7718 100644 --- a/tls/s2n_connection.c +++ b/tls/s2n_connection.c @@ -675,6 +675,28 @@ int s2n_connection_get_cipher_preferences(struct s2n_connection *conn, const str return 0; } +int s2n_connection_get_certificate_match(struct s2n_connection *conn, s2n_cert_sni_match *match_status) +{ + POSIX_ENSURE(conn, S2N_ERR_INVALID_ARGUMENT); + POSIX_ENSURE(match_status, S2N_ERR_INVALID_ARGUMENT); + POSIX_ENSURE(conn->mode == S2N_SERVER, S2N_ERR_CLIENT_MODE); + + /* Server must have gotten past certificate selection */ + POSIX_ENSURE(conn->handshake_params.our_chain_and_key, S2N_ERR_NO_CERT_FOUND); + + if (!s2n_server_received_server_name(conn)) { + *match_status = S2N_SNI_NONE; + } else if (conn->handshake_params.exact_sni_match_exists) { + *match_status = S2N_SNI_EXACT_MATCH; + } else if (conn->handshake_params.wc_sni_match_exists) { + *match_status = S2N_SNI_WILDCARD_MATCH; + } else { + *match_status = S2N_SNI_NO_MATCH; + } + + return S2N_SUCCESS; +} + int s2n_connection_get_security_policy(struct s2n_connection *conn, const struct s2n_security_policy **security_policy) { POSIX_ENSURE_REF(conn);