Repository: aws/s2n-tls (2025-08-25T16:58:39Z)

Issue:
s2n-tls server reports signature scheme with RSA kex
### Security issue notifications

If you discover a potential security issue in s2n we ask that you notify
AWS Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.

### Problem:

When a TLS handshake uses RSA kex, no separate signature is required. The server proves its ownership of the RSA certificate by encrypting/decrypting the premaster secret instead.

If you negotiate with RSA kex, you will see the reasonable output from s2nc:
```
s2nc localhost 8000 --ciphers test_all_rsa_kex
libcrypto: AWS-LC 1.49.1
CONNECTED:
Handshake: NEGOTIATED|FULL_HANDSHAKE|WITH_SESSION_TICKET
Client hello version: 33
Client protocol version: 33
Server protocol version: 33
Actual protocol version: 33
Server name: localhost
Curve: NONE
Cipher negotiated: AES128-SHA
Server signature negotiated: None+None <----HERE
Early Data status: NOT REQUESTED
Wire bytes in: 2704
Wire bytes out: 498
s2n is ready
Connected to localhost:8000
```
But on the s2nd side, we do report a signature:
```
s2nd localhost 8000 --ciphers test_all_rsa_kex
libcrypto: AWS-LC 1.49.1
Listening on localhost:8000
CONNECTED:
Handshake: NEGOTIATED|FULL_HANDSHAKE|WITH_SESSION_TICKET
Client hello version: 33
Client protocol version: 33
Server protocol version: 33
Actual protocol version: 33
Server name: localhost
Curve: NONE
Cipher negotiated: AES128-SHA
Server signature negotiated: RSA+SHA256 <----HERE
Early Data status: NOT REQUESTED
JA3: a24955ad4bc89eb0c960d34b2a6f1486
Wire bytes in: 498
Wire bytes out: 2704
s2n is ready
```

I'm guessing the problem is that the server always chooses a signature scheme as part of negotiating parameters. The client doesn't know what that scheme is because the server never actually send a signature.

### Need By Date:
None, no customer reach outs. I think this is just a visibility / logging / metrics issue.

### Solution:

We should make the server also report "None+None". Likely that means updating the signature algorithm selection logic to check for RSA kex.

* **Does this change what S2N sends over the wire?** If yes, explain.
* **Does this change any public APIs?** If yes, explain.
* **Which versions of TLS will this impact?**

### Requirements / Acceptance Criteria:

What must a solution address in order to solve the problem? How do we know the solution is complete?

* **RFC links:** Links to relevant RFC(s)
* **Related Issues:** Link any relevant issues
* **Will the Usage Guide or other documentation need to be updated?**
* **Testing:** How will this change be tested? Call out new integration tests, functional tests, or particularly interesting/important unit tests.
  * **Will this change trigger SAW changes?** Changes to the state machine, the s2n_handshake_io code that controls state transitions, the DRBG, or the corking/uncorking logic could trigger SAW failures.
  * **Should this change be fuzz tested?** Will it handle untrusted input? Create a separate issue to track the fuzzing work.

### Out of scope:

Is there anything the solution will intentionally NOT address?

[//]: #  (NOTE: If you believe this might be a security issue, please email aws-security@amazon.com instead of creating a GitHub issue. For more details, see the AWS Vulnerability Reporting Guide: https://aws.amazon.com/security/vulnerability-reporting/ )



Discussion:

Patch:
diff --git a/tls/s2n_auth_selection.c b/tls/s2n_auth_selection.c
index 5678f4ae82a..6971f9495e3 100644
--- a/tls/s2n_auth_selection.c
+++ b/tls/s2n_auth_selection.c
@@ -207,11 +207,17 @@ int s2n_is_cert_type_valid_for_auth(struct s2n_connection *conn, s2n_pkey_type c
 int s2n_select_certs_for_server_auth(struct s2n_connection *conn, struct s2n_cert_chain_and_key **chosen_certs)
 {
     POSIX_ENSURE_REF(conn);
-    POSIX_ENSURE_REF(conn->handshake_params.server_cert_sig_scheme);
-    s2n_signature_algorithm sig_alg = conn->handshake_params.server_cert_sig_scheme->sig_alg;
+    const struct s2n_signature_scheme *sig_scheme = conn->handshake_params.server_cert_sig_scheme;
+    POSIX_ENSURE_REF(sig_scheme);
+    s2n_signature_algorithm sig_alg = sig_scheme->sig_alg;
 
     s2n_pkey_type cert_type = S2N_PKEY_TYPE_UNKNOWN;
-    POSIX_GUARD_RESULT(s2n_signature_algorithm_get_pkey_type(sig_alg, &cert_type));
+    if (sig_scheme == &s2n_null_sig_scheme) {
+        /* Only RSA auth (+ RSA kex) supports no signature scheme */
+        cert_type = S2N_PKEY_TYPE_RSA;
+    } else {
+        POSIX_GUARD_RESULT(s2n_signature_algorithm_get_pkey_type(sig_alg, &cert_type));
+    }
 
     *chosen_certs = s2n_get_compatible_cert_chain_and_key(conn, cert_type);
     S2N_ERROR_IF(*chosen_certs == NULL, S2N_ERR_CERT_TYPE_UNSUPPORTED);
diff --git a/tls/s2n_signature_algorithms.c b/tls/s2n_signature_algorithms.c
index 749c4276df1..a728fa84d43 100644
--- a/tls/s2n_signature_algorithms.c
+++ b/tls/s2n_signature_algorithms.c
@@ -188,6 +188,12 @@ S2N_RESULT s2n_signature_algorithm_select(struct s2n_connection *conn)
         chosen_sig_scheme = &conn->handshake_params.server_cert_sig_scheme;
     }
 
+    /* No server signature is needed for RSA kex */
+    if (conn->mode == S2N_SERVER && cipher_suite->key_exchange_alg == &s2n_rsa) {
+        RESULT_ENSURE_EQ(*chosen_sig_scheme, &s2n_null_sig_scheme);
+        return S2N_RESULT_OK;
+    }
+
     /* Before TLS1.2, signature algorithms were fixed instead of negotiated */
     if (conn->actual_protocol_version < S2N_TLS12) {
         RESULT_GUARD(s2n_signature_algorithms_get_legacy_default(conn, conn->mode, chosen_sig_scheme));


Test patch:
diff --git a/tests/integrationv2/common.py b/tests/integrationv2/common.py
index 5b526a27fbd..26671136e91 100644
--- a/tests/integrationv2/common.py
+++ b/tests/integrationv2/common.py
@@ -634,6 +634,7 @@ def __str__(self):
 
 
 class Signatures(object):
+    NONE = Signature("None+None", max_protocol=Protocols.TLS13)
     RSA_SHA1 = Signature("RSA+SHA1", max_protocol=Protocols.TLS12)
     RSA_SHA224 = Signature("RSA+SHA224", max_protocol=Protocols.TLS12)
     RSA_SHA256 = Signature("RSA+SHA256", max_protocol=Protocols.TLS12)
diff --git a/tests/integrationv2/test_signature_algorithms.py b/tests/integrationv2/test_signature_algorithms.py
index 93f507d568b..22e9f8574e8 100644
--- a/tests/integrationv2/test_signature_algorithms.py
+++ b/tests/integrationv2/test_signature_algorithms.py
@@ -33,8 +33,20 @@
 ]
 
 
-def expected_signature(protocol, signature):
-    if protocol < Protocols.TLS12:
+def signature_marker(mode, protocol, cipher, signature):
+    # In versions before TLS1.3, the server uses the negotiated signature scheme for the
+    # ServerKeyExchange message. The server only sends the ServerKeyExchange message when using
+    # a key exchange method that provides forward secrecy, ie, NOT static RSA.
+    # So if using RSA key exchange, there is no actual "negotiated" signature scheme, because
+    # the server never sends the client a signature scheme.
+    #
+    # This mostly has to be inferred from the RFCs, but this blog post is a pretty good summary
+    # of the situation: https://timtaubert.de/blog/2016/07/the-evolution-of-signatures-in-tls/
+    if mode == Provider.ServerMode and cipher.iana_standard_name.startswith(
+        "TLS_RSA_WITH_"
+    ):
+        signature = Signatures.NONE
+    elif protocol < Protocols.TLS12:
         # ECDSA by default hashes with SHA-1.
         #
         # This is inferred from extended version of TLS1.1 rfc- https://www.rfc-editor.org/rfc/rfc4492#section-5.10
@@ -42,10 +54,7 @@ def expected_signature(protocol, signature):
             signature = Signatures.ECDSA_SHA1
         else:
             signature = Signatures.RSA_MD5_SHA1
-    return signature
-
 
-def signature_marker(mode, signature):
     return to_bytes(
         "{mode} signature negotiated: {type}+{digest}".format(
             mode=mode.title(), type=signature.sig_type, digest=signature.sig_digest
@@ -146,15 +155,11 @@ def test_s2n_server_signature_algorithms(
             in results.stdout
         )
         assert (
-            signature_marker(
-                Provider.ServerMode, expected_signature(protocol, signature)
-            )
+            signature_marker(Provider.ServerMode, protocol, cipher, signature)
             in results.stdout
         )
         assert (
-            signature_marker(
-                Provider.ClientMode, expected_signature(protocol, signature)
-            )
+            signature_marker(Provider.ClientMode, protocol, cipher, signature)
             in results.stdout
         ) == client_auth
         assert random_bytes in results.stdout
@@ -224,16 +229,6 @@ def test_s2n_client_signature_algorithms(
 
     expected_version = get_expected_s2n_version(protocol, provider)
 
-    # In versions before TLS1.3, the server uses the negotiated signature scheme for the
-    # ServerKeyExchange message. The server only sends the ServerKeyExchange message when using
-    # a key exchange method that provides forward secrecy, ie, NOT static RSA.
-    # So if using RSA key exchange, there is no actual "negotiated" signature scheme, because
-    # the server never sends the client a signature scheme.
-    #
-    # This mostly has to be inferred from the RFCs, but this blog post is a pretty good summary
-    # of the situation: https://timtaubert.de/blog/2016/07/the-evolution-of-signatures-in-tls/
-    server_sigalg_used = not cipher.iana_standard_name.startswith("TLS_RSA_WITH_")
-
     for results in client.get_results():
         results.assert_success()
         assert (
@@ -241,15 +236,10 @@ def test_s2n_client_signature_algorithms(
             in results.stdout
         )
         assert (
-            signature_marker(
-                Provider.ServerMode, expected_signature(protocol, signature)
-            )
+            signature_marker(Provider.ServerMode, protocol, cipher, signature)
             in results.stdout
-            or not server_sigalg_used
         )
         assert (
-            signature_marker(
-                Provider.ClientMode, expected_signature(protocol, signature)
-            )
+            signature_marker(Provider.ClientMode, protocol, cipher, signature)
             in results.stdout
         ) == client_auth
diff --git a/tests/testlib/s2n_testlib.h b/tests/testlib/s2n_testlib.h
index 74909154abe..223f09004b1 100644
--- a/tests/testlib/s2n_testlib.h
+++ b/tests/testlib/s2n_testlib.h
@@ -220,12 +220,12 @@ int s2n_test_cert_chain_and_key_new(struct s2n_cert_chain_and_key **chain_and_ke
  * See https://github.com/aws/s2n-tls/issues/4651 for more information.
  */
 int s2n_test_cert_permutation_load_server_chain(struct s2n_cert_chain_and_key **chain_and_key,
-        const char *type, const char *siganture, const char *size, const char *digest);
+        const char *type, const char *signature, const char *size, const char *digest);
 
-int s2n_test_cert_permutation_get_ca_path(char *output, const char *type, const char *siganture,
+int s2n_test_cert_permutation_get_ca_path(char *output, const char *type, const char *signature,
         const char *size, const char *digest);
 S2N_RESULT s2n_test_cert_permutation_get_server_chain_path(char *output, const char *type,
-        const char *siganture, const char *size, const char *digest);
+        const char *signature, const char *size, const char *digest);
 
 S2N_RESULT s2n_test_cert_chain_data_from_pem(struct s2n_connection *conn, const char *pem_path,
         struct s2n_stuffer *cert_chain_stuffer);
diff --git a/tests/unit/s2n_client_signature_algorithms_extension_test.c b/tests/unit/s2n_client_signature_algorithms_extension_test.c
index 93942a52b12..504c81e0d9f 100644
--- a/tests/unit/s2n_client_signature_algorithms_extension_test.c
+++ b/tests/unit/s2n_client_signature_algorithms_extension_test.c
@@ -81,6 +81,7 @@ int main(int argc, char **argv)
         EXPECT_NOT_NULL(conn = s2n_connection_new(S2N_SERVER));
         POSIX_GUARD(s2n_connection_set_config(conn, config));
         conn->actual_protocol_version = S2N_TLS12;
+        conn->secure->cipher_suite = &s2n_ecdhe_rsa_with_aes_256_gcm_sha384;
 
         struct s2n_stuffer signature_algorithms_extension = { 0 };
         EXPECT_SUCCESS(s2n_stuffer_alloc(&signature_algorithms_extension, 2 + (sig_hash_algs.len * 2)));
diff --git a/tests/unit/s2n_handshake_test.c b/tests/unit/s2n_handshake_test.c
index 63ff2cd2a1f..a51ae559aa5 100644
--- a/tests/unit/s2n_handshake_test.c
+++ b/tests/unit/s2n_handshake_test.c
@@ -129,7 +129,7 @@ static int try_handshake(struct s2n_connection *server_conn, struct s2n_connecti
 }
 
 int test_cipher_preferences(struct s2n_config *server_config, struct s2n_config *client_config,
-        struct s2n_cert_chain_and_key *expected_cert_chain, s2n_signature_algorithm expected_sig_alg)
+        struct s2n_cert_chain_and_key *expected_cert_chain, s2n_signature_algorithm sig_alg)
 {
     const struct s2n_security_policy *security_policy = server_config->security_policy;
     EXPECT_NOT_NULL(security_policy);
@@ -154,6 +154,12 @@ int test_cipher_preferences(struct s2n_config *server_config, struct s2n_config
             expect_failure = 1;
         }
 
+        s2n_signature_algorithm expected_sig_alg = sig_alg;
+        /* Expect no server signature algorithm if RSA kex */
+        if (expected_cipher->key_exchange_alg == &s2n_rsa) {
+            expected_sig_alg = S2N_SIGNATURE_ANONYMOUS;
+        }
+
         TEST_DEBUG_PRINT("Testing %s in %s mode, expect_failure=%d\n", expected_cipher->name,
                 test_type == TEST_TYPE_SYNC ? "synchronous" : "asynchronous", expect_failure);
 
diff --git a/tests/unit/s2n_signature_algorithms_test.c b/tests/unit/s2n_signature_algorithms_test.c
index fdbdf36c0f0..3bf2fb16b12 100644
--- a/tests/unit/s2n_signature_algorithms_test.c
+++ b/tests/unit/s2n_signature_algorithms_test.c
@@ -30,9 +30,10 @@
 #define LENGTH       (s2n_array_len(test_signature_schemes))
 #define STUFFER_SIZE (LENGTH * TLS_SIGNATURE_SCHEME_LEN + 10)
 
-#define RSA_CIPHER_SUITE   &s2n_ecdhe_rsa_with_aes_128_cbc_sha
-#define ECDSA_CIPHER_SUITE &s2n_ecdhe_ecdsa_with_aes_128_cbc_sha
-#define TLS13_CIPHER_SUITE &s2n_tls13_aes_128_gcm_sha256
+#define RSA_CIPHER_SUITE     &s2n_ecdhe_rsa_with_aes_128_cbc_sha
+#define RSA_KEX_CIPHER_SUITE &s2n_rsa_with_aes_256_gcm_sha384
+#define ECDSA_CIPHER_SUITE   &s2n_ecdhe_ecdsa_with_aes_128_cbc_sha
+#define TLS13_CIPHER_SUITE   &s2n_tls13_aes_128_gcm_sha256
 
 /* The only TLS1.3-only signature schemes are RSA-PSS-PSS, which
  * are difficult to test with due to mixed libcrypto support.
@@ -234,6 +235,36 @@ int main(int argc, char **argv)
         const struct s2n_signature_scheme *ecdsa_default = &s2n_ecdsa_sha1;
         const struct s2n_signature_scheme *rsa_default = &s2n_rsa_pkcs1_sha1;
 
+        /* Test: do not choose a server signature scheme for RSA kex */
+        {
+            DEFER_CLEANUP(struct s2n_connection *conn = s2n_connection_new(S2N_SERVER),
+                    s2n_connection_ptr_free);
+            EXPECT_SUCCESS(s2n_connection_set_config(conn, client_rsa_config));
+
+            /* Support a wide variety of signature schemes */
+            const struct s2n_signature_scheme *test_schemes[] = {
+                &s2n_ecdsa_sha384,
+                &s2n_rsa_pss_rsae_sha256,
+                &s2n_rsa_pss_pss_sha256,
+                &s2n_rsa_pkcs1_sha256,
+            };
+            struct s2n_local_sig_schemes_context local_context = { 0 };
+            EXPECT_OK(s2n_test_set_local_sig_schemes(conn, &local_context,
+                    test_schemes, s2n_array_len(test_schemes)));
+            EXPECT_OK(s2n_test_set_peer_sig_schemes(&conn->handshake_params.peer_sig_scheme_list,
+                    test_schemes, s2n_array_len(test_schemes)));
+
+            /* Test: If RSA kex is chosen, a server signature scheme is NOT chosen. */
+            conn->secure->cipher_suite = RSA_KEX_CIPHER_SUITE;
+            EXPECT_OK(s2n_signature_algorithm_select(conn));
+            EXPECT_EQUAL(conn->handshake_params.server_cert_sig_scheme, &s2n_null_sig_scheme);
+
+            /* Test: A client signature scheme is chosen even for RSA kex */
+            conn->mode = S2N_CLIENT;
+            EXPECT_OK(s2n_signature_algorithm_select(conn));
+            EXPECT_NOT_EQUAL(conn->handshake_params.client_cert_sig_scheme, &s2n_null_sig_scheme);
+        };
+
         /* Test: choose legacy default for <TLS1.2 */
         {
             /* Both the client and server support other signature schemes-- we're
@@ -1498,6 +1529,40 @@ int main(int argc, char **argv)
         }
     }
 
+    /* Self-Talk test: no server signature scheme is chosen for RSA kex */
+    {
+        DEFER_CLEANUP(struct s2n_config *config = s2n_config_new(), s2n_config_ptr_free);
+        EXPECT_NOT_NULL(config);
+        EXPECT_SUCCESS(s2n_config_set_unsafe_for_testing(config));
+        EXPECT_SUCCESS(s2n_config_add_cert_chain_and_key_to_store(config, rsa_cert_chain));
+        EXPECT_SUCCESS(s2n_config_set_cipher_preferences(config, "test_all_rsa_kex"));
+        EXPECT_SUCCESS(s2n_config_set_client_auth_type(config, S2N_CERT_AUTH_REQUIRED));
+
+        DEFER_CLEANUP(struct s2n_connection *client = s2n_connection_new(S2N_CLIENT),
+                s2n_connection_ptr_free);
+        EXPECT_SUCCESS(s2n_connection_set_config(client, config));
+
+        DEFER_CLEANUP(struct s2n_connection *server = s2n_connection_new(S2N_SERVER),
+                s2n_connection_ptr_free);
+        EXPECT_SUCCESS(s2n_connection_set_config(server, config));
+
+        DEFER_CLEANUP(struct s2n_test_io_stuffer_pair io_pair, s2n_io_stuffer_pair_free);
+        EXPECT_OK(s2n_io_stuffer_pair_init(&io_pair));
+        EXPECT_OK(s2n_connections_set_io_stuffer_pair(client, server, &io_pair));
+        EXPECT_SUCCESS(s2n_negotiate_test_server_and_client(server, client));
+
+        /* RSA kex negotiated */
+        EXPECT_EQUAL(server->secure->cipher_suite->key_exchange_alg, &s2n_rsa);
+
+        /* No server signature scheme */
+        EXPECT_EQUAL(server->handshake_params.server_cert_sig_scheme, &s2n_null_sig_scheme);
+        EXPECT_EQUAL(client->handshake_params.server_cert_sig_scheme, &s2n_null_sig_scheme);
+
+        /* Client signature scheme negotiated */
+        EXPECT_EQUAL(server->handshake_params.client_cert_sig_scheme, &s2n_rsa_pkcs1_sha256);
+        EXPECT_EQUAL(client->handshake_params.client_cert_sig_scheme, &s2n_rsa_pkcs1_sha256);
+    };
+
     END_TEST();
 
     return 0;