Commit 2e49fe4ad8 for openssl.org
commit 2e49fe4ad8be3dc9a63a4533c404e0a2728ea8a3
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date: Sat Jul 4 13:58:00 2026 +0200
statem: test tls_construct_client_certificate
Exercise the certificate output functions under mfail with real key
material embedded in the test: an x509 chain over TLS 1.3 and an RPK over
TLS 1.2. Add deterministic tests for the error branches mfail cannot
reach: an unknown certificate type, a failed write-key change, and
WPACKET failures writing the TLS 1.3 context.
Assisted-by: Claude:claude-opus-4-8
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
MergeDate: Mon Jul 20 09:11:48 2026
(Merged from https://github.com/openssl/openssl/pull/31861)
diff --git a/test/statem_clnt_construct_test.c b/test/statem_clnt_construct_test.c
index debedb3af1..10f58a3ab1 100644
--- a/test/statem_clnt_construct_test.c
+++ b/test/statem_clnt_construct_test.c
@@ -8,10 +8,11 @@
*/
/*
- * Direct tests for tls_construct_client_hello(): prime a client SSL_CONNECTION
- * enough to call the construct function without a full handshake, then check
- * the result structurally and by round-tripping it through the server parser.
- * OOM branches are covered with mfail tests.
+ * Direct tests for the client state-machine construct functions in
+ * statem_clnt.c: prime a client SSL_CONNECTION enough to call a construct
+ * function without a full handshake, then check the result structurally and,
+ * where useful, by round-tripping it through the server parser. OOM branches
+ * are covered with mfail tests.
*/
#include <openssl/ssl.h>
@@ -41,8 +42,8 @@
#endif
/*
- * Helpers down to prime_ssl() are generic and reusable by tests for any
- * statem_clnt construct function; the ClientHello-specific code follows.
+ * Helpers down to finish_msg() are generic and reusable by tests for any
+ * statem_clnt construct function; the per-message code follows.
*/
/* Connection configuration shared by the construct tests. */
@@ -743,6 +744,301 @@ static int test_construct_eoed_bad_state(void)
}
#endif /* OSSL_NO_USABLE_TLS1_3 */
+/*
+ * ===========================================================================
+ * tls_construct_client_certificate
+ * ===========================================================================
+ */
+
+#if !defined(OSSL_NO_USABLE_TLS1_3) || !defined(OPENSSL_NO_TLS1_2)
+/* Self-signed client cert + signing-capable key; regenerate with the
+ * statem_clnt_construct_test ossl-test-tools subcommand. */
+static const char *kClientCert[] = {
+ "-----BEGIN CERTIFICATE-----\n",
+ "MIIDvzCCAqegAwIBAgICAQAwDQYJKoZIhvcNAQELBQAwgYExCzAJBgNVBAYTAlVT\n",
+ "MRAwDgYDVQQIDAdXeW9taW5nMREwDwYDVQQHDAhDaGV5ZW5uZTEVMBMGA1UECgwM\n",
+ "T3BlblNTTCBUZXN0MRQwEgYDVQQLDAtzdGF0ZW1fY2xudDEgMB4GA1UEAwwXc3Rh\n",
+ "dGVtX2NsbnQgdGVzdCBjbGllbnQwHhcNMjYwMTAxMDAwMDAwWhcNNDYwMTAxMDAw\n",
+ "MDAwWjCBgTELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB1d5b21pbmcxETAPBgNVBAcM\n",
+ "CENoZXllbm5lMRUwEwYDVQQKDAxPcGVuU1NMIFRlc3QxFDASBgNVBAsMC3N0YXRl\n",
+ "bV9jbG50MSAwHgYDVQQDDBdzdGF0ZW1fY2xudCB0ZXN0IGNsaWVudDCCASIwDQYJ\n",
+ "KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMnUvJvluB2ZUoQNQlW4wgv0qceITB5X\n",
+ "cHQe60H1CMTapaRi32dpwEzoEMnMjULcrZshcTAkdke6J1ubJ6qviGp7n1kVYH18\n",
+ "rGYYk6VT+GPb/SZnjMX3+e5WEpH+53UEGVvBPHl/med0AzklOOf/0hDlMFzMBejA\n",
+ "z+T++88QIT19BoIwfilcMDZxE0uXbq3QLpugADGd93zLSCwM1vxd9Vi0EwyMpy7Q\n",
+ "Ot9eIR/+ML0HESXZ1AvVcLjvuhqm+xkNiR9qil68zqgJk+dUpK5hCpLBi7cfBpk7\n",
+ "jLultF09up6G3Y5KiXd8wS9upwJZXA9+9OKHTf05w4xWAA/kpp9gt0MCAwEAAaM/\n",
+ "MD0wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFM9Nespo\n",
+ "NSyfQO8jPjRl4JfH5kOsMA0GCSqGSIb3DQEBCwUAA4IBAQBtTgy5ePCHR+iu3Ign\n",
+ "wlJzL5+zkWOkQsbAzJsbWrvzqwx2shXr1adM7OJy7tCkmDgwHsXjTTO2qAZrlmYQ\n",
+ "ktGA/UAtttIqgiiYcyGdrZas2vXUWLUps5YzMm4YdY8YNTvqQl3LCziUhO5YREDP\n",
+ "teXy4FF6ijGUDe84CYsmKvtbIn34LtZ2Vo3gsiRHvdiaxHavH30UqED9k4NjKnFx\n",
+ "XM6eMw+bs0Yl1vi/Dz5tHPRaAnsGvnKcEveSAdAoSWmodw8n8W5t8ZvPoPCoKz88\n",
+ "DOGzUvLma/kVL+3HLiSn6XFiQ2NLfO9ceUgDsSzcRo76XZHJnfGsK6Iewgje3NrB\n",
+ "WnZs\n",
+ "-----END CERTIFICATE-----\n",
+ NULL
+};
+
+static const char *kClientKey[] = {
+ "-----BEGIN RSA PRIVATE KEY-----\n",
+ "MIIEowIBAAKCAQEAydS8m+W4HZlShA1CVbjCC/Spx4hMHldwdB7rQfUIxNqlpGLf\n",
+ "Z2nATOgQycyNQtytmyFxMCR2R7onW5snqq+IanufWRVgfXysZhiTpVP4Y9v9JmeM\n",
+ "xff57lYSkf7ndQQZW8E8eX+Z53QDOSU45//SEOUwXMwF6MDP5P77zxAhPX0GgjB+\n",
+ "KVwwNnETS5durdAum6AAMZ33fMtILAzW/F31WLQTDIynLtA6314hH/4wvQcRJdnU\n",
+ "C9VwuO+6Gqb7GQ2JH2qKXrzOqAmT51SkrmEKksGLtx8GmTuMu6W0XT26nobdjkqJ\n",
+ "d3zBL26nAllcD3704odN/TnDjFYAD+Smn2C3QwIDAQABAoIBAGAjkzIJczG6MmmP\n",
+ "bU0q5FfQk7zlaii7yuetQK/a2fH3GpbauALpBz46/qA5bQJv3sw52lIt1B+nhw7m\n",
+ "MbdmxKrANy+2dI9hvzckttO2U2exxvyvr4kvbWB/pHnhu3vsV23y9m0DgJqVEuH6\n",
+ "Hoi4PWZp3aceUiRED+NLKERCMSs5lPSSWR7sUkzHku4x5fZXZppcQW4leo+Z/kNC\n",
+ "I36CGF4pI9FJXwcuRhbv3NsFMVl/Ng4hWgzgu7zwJEPw2OSKyhdwHV53qGQWyMnJ\n",
+ "7SawyQfyspKlMZnjWxplFZ5tgaV6O63zZZPEOC2mZNeiZKWC9Lut/wyriLs+W4w7\n",
+ "9BBX3q0CgYEA94sgemr3rGY2R+9kiPV/TC08IUflMJ7kt0epDDRnojWqobinJzQG\n",
+ "Nq25i5vZHbjn9g7l2hNmOcHVZZmCYZitmjwWr4ibthftU7+H6WkvcBvH6b6EmHQm\n",
+ "5IGtOxYPmtf2Ghnj1uQmsBe9vRYcqx7B8/anqRi3lQhebykkII0MLR8CgYEA0LnV\n",
+ "vXaFAwLOta4Kn43mXC6sVRRMt316d70zRpVRsSQ64TdhtwArmKq3RUmLH2r3ysRL\n",
+ "6+mxEGJriL7JERH3Jlm0YGsUUQvQWxCddccuTQRc16+UVt7+xzfhzJZNTD4+t0aA\n",
+ "jsVLpPQHzXI8Yqzh1p83oC4XV4hCYZNLlfleTV0CgYBWIG/yZ9k4gG+OY7pk9JWP\n",
+ "2YU8Rxl06zPEmQg2GN2d0HJHxklSGIW47ITMEDNgZf8+2zwZvfopSkmHCfwVHNv5\n",
+ "98Ik3LDgkD6gjtko2tIIfYH2z7SunmsRwhSVpD1VsKINvshI8iSLzBbV/SWIXDE7\n",
+ "Qqxe5xyom7rPjk7ljG2aHQKBgB36oxV8YWxmSdRUdBgopG6XEY+Cw+YS8rUiCqxX\n",
+ "pA0iXAafErzbHGfoFTyxbHcNwRtxiEoRHapxyGoypOR7xRjQB5VVq+xcGwgJYeRZ\n",
+ "wG+1cbRU9qRnkQaCIz9kUyPhSNbAHJTlB5Fgr4I1pzCxDhrqcW3jUNz0qDwlkNSw\n",
+ "pXfNAoGBAKlFbbPEzFonlkHVtdUuk6Chsi5k/ddrWsGxwUw5F5BSu0UaseolVls5\n",
+ "TyDVa8FEfDnUZRxl8HSfC/Qp/kAdveGKyhNaex22L5G8m20rXjsQBMFHMOy2Mho1\n",
+ "hgs0/emKuVyCs+wnYOqJlWZ8Vf/qGcUtDF3r4aEZ1JUDhUBAVEpo\n",
+ "-----END RSA PRIVATE KEY-----\n",
+ NULL
+};
+
+static int load_cert_and_key(SSL *ssl)
+{
+ X509 *cert = NULL;
+ EVP_PKEY *pkey = NULL;
+ int ret = 0;
+
+ if (!TEST_ptr(cert = X509_from_strings(kClientCert))
+ || !TEST_ptr(pkey = PKEY_from_strings(kClientKey))
+ || !TEST_int_eq(SSL_use_certificate(ssl, cert), 1)
+ || !TEST_int_eq(SSL_use_PrivateKey(ssl, pkey), 1))
+ goto err;
+ ret = 1;
+err:
+ X509_free(cert);
+ EVP_PKEY_free(pkey);
+ return ret;
+}
+
+/*
+ * Run tls_construct_client_certificate() under mfail; prep installs the cert
+ * material. For TLS 1.3 the method is swapped in (IS_TLS13 keys off it) and
+ * middlebox compat cleared to skip the write-key change.
+ */
+static int mfail_construct_cert(int is_tls13, int (*prep)(SSL_CONNECTION *s))
+{
+ CH_CONFIG cfg = { 0, 0, 0, 0 };
+ SSL_CTX *cctx = NULL;
+ SSL *ssl = NULL;
+ SSL_CONNECTION *s;
+ WPACKET pkt;
+ int ok = 0;
+ int ret = 0;
+
+ if (!TEST_ptr(cctx = new_ctx(&cfg, client_method(&cfg)))
+ || !TEST_ptr(ssl = SSL_new(cctx)))
+ goto err;
+ if (is_tls13)
+ SSL_clear_options(ssl, SSL_OP_ENABLE_MIDDLEBOX_COMPAT);
+ if (!prime_ssl(ssl, 1, 0, &pkt, SSL3_MT_CERTIFICATE))
+ goto err;
+ s = SSL_CONNECTION_FROM_SSL(ssl);
+#ifndef OSSL_NO_USABLE_TLS1_3
+ if (is_tls13)
+ ssl->method = tlsv1_3_client_method();
+#endif
+
+ if (prep != NULL && !prep(s)) {
+ WPACKET_cleanup(&pkt);
+ goto err;
+ }
+
+ MFAIL_start();
+ ok = (tls_construct_client_certificate(s, &pkt) == CON_FUNC_SUCCESS);
+ MFAIL_end();
+
+ WPACKET_cleanup(&pkt);
+
+ ret = ok ? 1 : 0;
+err:
+ SSL_free(ssl);
+ SSL_CTX_free(cctx);
+ return ret;
+}
+#endif /* TLS 1.2 or usable TLS 1.3 */
+
+#ifndef OSSL_NO_USABLE_TLS1_3
+/* x509 over TLS 1.3; NO_AUTO_CHAIN avoids best-effort verify swallowing OOM. */
+static int prep_cert_x509(SSL_CONNECTION *s)
+{
+ SSL *ssl = SSL_CONNECTION_GET_SSL(s);
+
+ SSL_set_mode(ssl, SSL_MODE_NO_AUTO_CHAIN);
+ return load_cert_and_key(ssl);
+}
+
+static int mfail_construct_cert_x509(void)
+{
+ return mfail_construct_cert(1, prep_cert_x509);
+}
+#endif /* OSSL_NO_USABLE_TLS1_3 */
+
+#ifndef OPENSSL_NO_TLS1_2
+/* RPK derived from the certificate public key over TLS 1.2 (tls_output_rpk). */
+static int prep_cert_rpk(SSL_CONNECTION *s)
+{
+ if (!load_cert_and_key(SSL_CONNECTION_GET_SSL(s)))
+ return 0;
+ s->ext.client_cert_type = TLSEXT_cert_type_rpk;
+ return 1;
+}
+
+static int mfail_construct_cert_rpk(void)
+{
+ return mfail_construct_cert(0, prep_cert_rpk);
+}
+#endif /* OPENSSL_NO_TLS1_2 */
+
+/* Deterministic error branches that mfail (allocation-only) cannot reach. */
+
+/* An unrecognized certificate type is rejected. */
+static int test_construct_cert_bad_type(void)
+{
+ CH_CONFIG cfg = { 0, 0, 0, 0 };
+ SSL_CTX *cctx = NULL;
+ SSL *ssl = NULL;
+ SSL_CONNECTION *s;
+ WPACKET pkt;
+ int have_pkt = 0;
+ int ret = 0;
+
+ if (!TEST_ptr(cctx = new_ctx(&cfg, client_method(&cfg)))
+ || !TEST_ptr(ssl = SSL_new(cctx))
+ || !prime_ssl(ssl, 1, 0, &pkt, SSL3_MT_CERTIFICATE))
+ goto err;
+ have_pkt = 1;
+ s = SSL_CONNECTION_FROM_SSL(ssl);
+ s->ext.client_cert_type = 0xff;
+
+ if (!TEST_int_eq(tls_construct_client_certificate(s, &pkt), CON_FUNC_ERROR))
+ goto err;
+
+ ret = 1;
+err:
+ if (have_pkt)
+ WPACKET_cleanup(&pkt);
+ SSL_free(ssl);
+ SSL_CTX_free(cctx);
+ return ret;
+}
+
+#ifndef OSSL_NO_USABLE_TLS1_3
+/*
+ * With middlebox compat on, the TLS 1.3 path changes the write keys; without a
+ * negotiated cipher that fails rather than succeeding.
+ */
+static int test_construct_cert_change_cipher_fail(void)
+{
+ CH_CONFIG cfg = { 0, 0, 0, 0 };
+ SSL_CTX *cctx = NULL;
+ SSL *ssl = NULL;
+ SSL_CONNECTION *s;
+ WPACKET pkt;
+ int have_pkt = 0;
+ int ret = 0;
+
+ if (!TEST_ptr(cctx = new_ctx(&cfg, client_method(&cfg)))
+ || !TEST_ptr(ssl = SSL_new(cctx))
+ || !prime_ssl(ssl, 1, 0, &pkt, SSL3_MT_CERTIFICATE))
+ goto err;
+ have_pkt = 1;
+ s = SSL_CONNECTION_FROM_SSL(ssl);
+ ssl->method = tlsv1_3_client_method();
+
+ if (!TEST_int_eq(tls_construct_client_certificate(s, &pkt), CON_FUNC_ERROR))
+ goto err;
+
+ ret = 1;
+err:
+ if (have_pkt)
+ WPACKET_cleanup(&pkt);
+ SSL_free(ssl);
+ SSL_CTX_free(cctx);
+ return ret;
+}
+
+/*
+ * A WPACKET failure while writing the TLS 1.3 certificate_request_context
+ * yields CON_FUNC_ERROR. with_pha exercises the non-empty-context branch.
+ */
+static int do_construct_cert_ctx_small_buf(int with_pha)
+{
+ CH_CONFIG cfg = { 0, 0, 0, 0 };
+ SSL_CTX *cctx = NULL;
+ SSL *ssl = NULL;
+ SSL_CONNECTION *s;
+ WPACKET pkt;
+ unsigned char buf[16];
+ int have_pkt = 0;
+ int ret = 0;
+
+ if (!TEST_ptr(cctx = new_ctx(&cfg, client_method(&cfg)))
+ || !TEST_ptr(ssl = SSL_new(cctx)))
+ goto err;
+ SSL_set_connect_state(ssl);
+ s = SSL_CONNECTION_FROM_SSL(ssl);
+ if (!TEST_ptr(s)
+ || !TEST_ptr(s->init_buf = BUF_MEM_new())
+ || !TEST_true(BUF_MEM_grow(s->init_buf, SSL3_RT_MAX_PLAIN_LENGTH))
+ || !TEST_true(tls_setup_handshake(s)))
+ goto err;
+ ssl->method = tlsv1_3_client_method();
+
+ if (with_pha) {
+ if (!TEST_ptr(s->pha_context = OPENSSL_malloc(4)))
+ goto err;
+ s->pha_context_len = 4;
+ }
+
+ /* Only the handshake header fits, so the context write overflows. */
+ if (!TEST_true(WPACKET_init_static_len(&pkt, buf, hdr_len(&cfg), 0)))
+ goto err;
+ have_pkt = 1;
+ if (!TEST_true(ssl_set_handshake_header(s, &pkt, SSL3_MT_CERTIFICATE)))
+ goto err;
+
+ if (!TEST_int_eq(tls_construct_client_certificate(s, &pkt), CON_FUNC_ERROR))
+ goto err;
+
+ ret = 1;
+err:
+ if (have_pkt)
+ WPACKET_cleanup(&pkt);
+ SSL_free(ssl);
+ SSL_CTX_free(cctx);
+ return ret;
+}
+
+static int test_construct_cert_ctx_small_buf(void)
+{
+ return do_construct_cert_ctx_small_buf(0);
+}
+
+static int test_construct_cert_pha_ctx_small_buf(void)
+{
+ return do_construct_cert_ctx_small_buf(1);
+}
+#endif /* OSSL_NO_USABLE_TLS1_3 */
+
int setup_tests(void)
{
ADD_TEST(test_construct_ch_small_buf);
@@ -787,5 +1083,19 @@ int setup_tests(void)
#endif
ADD_MFAIL_TEST(mfail_construct_ch_ech);
#endif /* OSSL_NO_USABLE_ECH */
+
+ /* tls_construct_client_certificate: OOM coverage of the output functions. */
+#ifndef OSSL_NO_USABLE_TLS1_3
+ ADD_MFAIL_TEST(mfail_construct_cert_x509);
+#endif
+#ifndef OPENSSL_NO_TLS1_2
+ ADD_MFAIL_TEST(mfail_construct_cert_rpk);
+#endif
+ ADD_TEST(test_construct_cert_bad_type);
+#ifndef OSSL_NO_USABLE_TLS1_3
+ ADD_TEST(test_construct_cert_change_cipher_fail);
+ ADD_TEST(test_construct_cert_ctx_small_buf);
+ ADD_TEST(test_construct_cert_pha_ctx_small_buf);
+#endif
return 1;
}