### Changes between 4.0 and 4.1 [xx XXX xxxx]
+ * EC key point format simplification.
+
+ The point conversion form (compressed, uncompressed, or hybrid)
+ is now a single value on the `EC_GROUP` and round-trips
+ unchanged through import and export of `EC_KEY` objects.
+
+ Freshly generated keys have their public point encoded in
+ uncompressed form. A `point-format` supplied at key generation
+ time via `OSSL_PKEY_PARAM_EC_POINT_CONVERSION_FORMAT` is
+ validated (an invalid value is rejected) but otherwise ignored
+ on the generated key. EC parameter generation continues to
+ honour the requested form on the group's generator; imported
+ keys keep their form.
+
+ The `ec_point_formats` extension no longer affects TLS 1.2
+ X.509 certificate selection or acceptance. OpenSSL now
+ accepts an EC certificate in any point form it can decode,
+ and sends any EC certificate it has regardless of point form.
+ TLS 1.3 disregards the extension entirely.
+
+ The RFC 4492/8422 section 5.1.2 requirement that the peer's
+ point-format list contain "uncompressed" is now enforced on
+ both sides (previously client-only), and only when an ECC
+ TLS 1.2 ciphersuite is negotiated -- a missing "uncompressed"
+ is ignored under TLS 1.3 or with a non-ECC cipher.
+
+ *Viktor Dukhovni*
+
* Added unit tests setup activated via `enable-unit-tests` option. This works
only on platforms with ld `--wrap` support (Linux, BSD).
*Adriano Sela Aviles*
* SubjectPublicKeyInfo blobs whose AlgorithmIdentifier uses id-RSAES-OAEP
- (NID_rsaesOaep, 1.2.840.113549.1.1.7) with a plain RSAPublicKey body
+ (`NID_rsaesOaep`, 1.2.840.113549.1.1.7) with a plain RSAPublicKey body
are now decoded as RSA keys. This is required for interoperability
with TPM 1.2 Endorsement Key certificates per TCG Credential Profiles
V1.2 section 3.2.7. The OAEP AlgorithmIdentifier parameters are not
SSL_CTX_set1_groups, SSL_CTX_set1_groups_list, SSL_set1_groups,
SSL_set1_groups_list, SSL_get1_groups, SSL_get0_iana_groups,
-SSL_get_shared_group, SSL_get_negotiated_group, SSL_CTX_set1_curves,
+SSL_get0_ec_point_formats, SSL_get_shared_group, SSL_get_negotiated_group,
+SSL_CTX_set1_curves,
SSL_CTX_set1_curves_list, SSL_set1_curves, SSL_set1_curves_list,
SSL_get1_curves, SSL_get_shared_curve, SSL_CTX_get0_implemented_groups
- EC supported curve functions
int SSL_get1_groups(SSL *ssl, int *groups);
int SSL_get0_iana_groups(SSL *ssl, uint16_t **out);
+ int SSL_get0_ec_point_formats(SSL *ssl, const unsigned char **plst);
int SSL_get_shared_group(SSL *s, int n);
int SSL_get_negotiated_group(SSL *s);
that was received in the ClientHello. The return value is the number of groups,
not the number of bytes written.
+SSL_get0_ec_point_formats() retrieves the peer's B<ec_point_formats>
+extension as a byte array of B<TLSEXT_ECPOINTFORMAT_*> values in the
+order received. If the peer sent the extension, B<*plst> is set to an
+internal buffer holding the list and its length is returned; otherwise
+the return value is 0 and B<*plst> is left unchanged. The returned
+pointer is owned by the B<SSL> connection and must not be freed. The
+point format no longer affects certificate selection or acceptance.
+
+The B<ec_point_formats> extension applies only to TLS 1.2 and below, but
+the peer's list is recorded even when TLS 1.3 is ultimately negotiated,
+since a TLS 1.3 B<ClientHello> may still carry the extension for
+backward compatibility.
+
SSL_get_shared_group() returns the NID of the shared group B<n> for a
server-side SSL B<ssl>. If B<n> is -1 then the total number of shared groups is
returned, which may be zero. Other than for diagnostic purposes,
SSL_get0_iana_groups() returns the number of (uint16_t) groups, which may be zero.
+SSL_get0_ec_point_formats() returns the number of point formats sent by
+the peer in its B<ec_point_formats> extension, or 0 if no such extension
+was received.
+
SSL_get_shared_group() returns the NID of shared group B<n> or NID_undef if there
is no shared group B<n>; or the total number of shared groups if B<n>
is -1.
goto err;
s->session_ctx = ctx;
- if (ctx->ext.ecpointformats != NULL) {
- s->ext.ecpointformats = OPENSSL_memdup(ctx->ext.ecpointformats,
- ctx->ext.ecpointformats_len);
- if (s->ext.ecpointformats == NULL) {
- s->ext.ecpointformats_len = 0;
- goto err;
- }
- s->ext.ecpointformats_len = ctx->ext.ecpointformats_len;
- }
if (ctx->ext.supportedgroups != NULL) {
size_t add = 0;
OPENSSL_free(s->ext.hostname);
SSL_CTX_free(s->session_ctx);
- OPENSSL_free(s->ext.ecpointformats);
OPENSSL_free(s->ext.peer_ecpointformats);
OPENSSL_free(s->ext.supportedgroups);
OPENSSL_free(s->ext.keyshares);
ssl_ctx_srp_ctx_free_intern(a);
#endif
- OPENSSL_free(a->ext.ecpointformats);
OPENSSL_free(a->ext.supportedgroups);
OPENSSL_free(a->ext.keyshares);
OPENSSL_free(a->ext.tuples);
#ifndef OSSL_SSL_LOCAL_H
#define OSSL_SSL_LOCAL_H
+#include <stdbool.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>
/* RFC 4366 Maximum Fragment Length Negotiation */
uint8_t max_fragment_len_mode;
- /* EC extension values inherited by SSL structure */
- size_t ecpointformats_len;
- unsigned char *ecpointformats;
-
size_t supportedgroups_len;
uint16_t *supportedgroups;
unsigned char *scts;
/* Length of raw extension data, if seen */
uint16_t scts_len;
- /* Expect OCSP CertificateStatus message */
- int status_expected;
struct {
/* OCSP status request only */
STACK_OF(OCSP_RESPONSE) *resp_ex;
} ocsp;
- /* RFC4507 session ticket expected to be received or sent */
- int ticket_expected;
/* TLS 1.3 tickets requested by the application. */
int extra_tickets_expected;
- /* our list */
- size_t ecpointformats_len;
- unsigned char *ecpointformats;
- /* peer's list */
+ /*
+ * Peer's advertised ec_point_formats list (TLS 1.2 and below),
+ * retained as received so SSL_get0_ec_point_formats() can return
+ * it verbatim. Point format no longer influences cert selection
+ * or acceptance; the parse hook validates RFC 4492/8422 section
+ * 5.1.2 ("uncompressed" must be present) inline.
+ */
size_t peer_ecpointformats_len;
unsigned char *peer_ecpointformats;
/* The available PSK key exchange modes */
int psk_kex_mode;
- /* Set to one if we have negotiated ETM */
- int use_etm;
-
/* Are we expecting to receive early data? */
int early_data;
- /* Is the session suitable for early data? */
- int early_data_ok;
/* May be sent by a server in HRR. Must be echoed back in ClientHello */
unsigned char *tls13_cookie;
size_t tls13_cookie_len;
- /* Have we received a cookie from the client? */
- int cookieok;
/*
* Maximum Fragment Length as per RFC 4366.
/* This is the list of algorithms the peer supports that we also support */
int compress_certificate_from_peer[TLSEXT_comp_cert_limit];
- /* indicate that we sent the extension, so we'll accept it */
- int compress_certificate_sent;
uint8_t client_cert_type;
uint8_t client_cert_type_ctos;
/* RFC 8701 GREASE */
uint8_t grease_seed[OSSL_GREASE_LAST_INDEX + 1];
- int grease_seeded;
+
+ /* "bool" fields go last, for slightly better packing */
+ bool grease_seeded;
+
+ /* Expect OCSP CertificateStatus message */
+ bool status_expected;
+
+ /* RFC4507 session ticket expected to be received or sent */
+ bool ticket_expected;
+
+ /* Set to one if we have negotiated ETM */
+ bool use_etm;
+
+ /* Is the session suitable for early data? */
+ bool early_data_ok;
+
+ /* Have we received a cookie from the client? */
+ bool cookieok;
+
+ /* indicate that we sent the extension, so we'll accept it */
+ bool compress_certificate_sent;
} ext;
/*
static int final_renegotiate(SSL_CONNECTION *s, unsigned int context, int sent);
static int init_server_name(SSL_CONNECTION *s, unsigned int context);
static int final_server_name(SSL_CONNECTION *s, unsigned int context, int sent);
-static int final_ec_pt_formats(SSL_CONNECTION *s, unsigned int context,
- int sent);
static int init_session_ticket(SSL_CONNECTION *s, unsigned int context);
#ifndef OPENSSL_NO_OCSP
static int init_status_request(SSL_CONNECTION *s, unsigned int context);
static int tls_parse_compress_certificate(SSL_CONNECTION *sc, PACKET *pkt,
unsigned int context,
X509 *x, size_t chainidx);
+static int tls_parse_ec_pt_formats(SSL_CONNECTION *s, PACKET *pkt,
+ unsigned int context,
+ X509 *x, size_t chainidx);
/* Structure to define a built-in extension */
typedef struct extensions_definition_st {
SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
| SSL_EXT_TLS1_2_AND_BELOW_ONLY,
OSSL_ECH_HANDLING_COMPRESS,
- init_ec_point_formats, tls_parse_ctos_ec_pt_formats, tls_parse_stoc_ec_pt_formats,
+ init_ec_point_formats, tls_parse_ec_pt_formats, tls_parse_ec_pt_formats,
tls_construct_stoc_ec_pt_formats, tls_construct_ctos_ec_pt_formats,
- final_ec_pt_formats },
+ NULL },
{ /*
* "supported_groups" is spread across several specifications.
* It was originally specified as "elliptic_curves" in RFC 4492,
return 1;
}
+/*
+ * Parse an ec_point_formats extension off the wire (one function for
+ * both sides). The peer's list is retained verbatim for
+ * SSL_get0_ec_point_formats() and for the RFC 4492/8422 section 5.1.2
+ * 'uncompressed must be present' check.
+ *
+ * The check is gated on the negotiated ciphersuite -- a TLS 1.3
+ * handshake or a non-ECC TLS 1.2 ciphersuite makes the extension moot
+ * and any missing 'uncompressed' codepoint is ignored. On the client
+ * the chosen ciphersuite is already locked in by the time we parse
+ * ServerHello, so the check happens inline here. On the server it's
+ * deferred to tls_construct_stoc_ec_pt_formats(), the first point at
+ * which s->s3.tmp.new_cipher is set for TLS 1.2.
+ */
+static int tls_parse_ec_pt_formats(SSL_CONNECTION *s, PACKET *pkt,
+ unsigned int context, X509 *x, size_t chainidx)
+{
+ PACKET list;
+
+ if (!PACKET_as_length_prefixed_1(pkt, &list)
+ || PACKET_remaining(&list) == 0) {
+ SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+ return 0;
+ }
+ if (!s->hit
+ && !PACKET_memdup(&list, &s->ext.peer_ecpointformats,
+ &s->ext.peer_ecpointformats_len)) {
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return 0;
+ }
+
+ if (!s->server) {
+ unsigned long alg_k = s->s3.tmp.new_cipher->algorithm_mkey;
+ unsigned long alg_a = s->s3.tmp.new_cipher->algorithm_auth;
+
+ if (((alg_k & SSL_kECDHE) || (alg_a & SSL_aECDSA))
+ && memchr(PACKET_data(&list),
+ TLSEXT_ECPOINTFORMAT_uncompressed,
+ PACKET_remaining(&list))
+ == NULL) {
+ SSLfatal(s, SSL_AD_ILLEGAL_PARAMETER,
+ SSL_R_TLS_INVALID_ECPOINTFORMAT_LIST);
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
/*
* Check whether the context defined for an extension |extctx| means whether
* the extension is relevant for the current context |thisctx| or not. Returns
}
}
-static int final_ec_pt_formats(SSL_CONNECTION *s, unsigned int context,
- int sent)
-{
- unsigned long alg_k, alg_a;
-
- if (s->server)
- return 1;
-
- alg_k = s->s3.tmp.new_cipher->algorithm_mkey;
- alg_a = s->s3.tmp.new_cipher->algorithm_auth;
-
- /*
- * If we are client and using an elliptic curve cryptography cipher
- * suite, then if server returns an EC point formats lists extension it
- * must contain uncompressed.
- */
- if (s->ext.ecpointformats != NULL
- && s->ext.ecpointformats_len > 0
- && s->ext.peer_ecpointformats != NULL
- && s->ext.peer_ecpointformats_len > 0
- && ((alg_k & SSL_kECDHE) || (alg_a & SSL_aECDSA))) {
- /* we are using an ECC cipher */
- size_t i;
- unsigned char *list = s->ext.peer_ecpointformats;
-
- for (i = 0; i < s->ext.peer_ecpointformats_len; i++) {
- if (*list++ == TLSEXT_ECPOINTFORMAT_uncompressed)
- break;
- }
- if (i == s->ext.peer_ecpointformats_len) {
- SSLfatal(s, SSL_AD_ILLEGAL_PARAMETER,
- SSL_R_TLS_INVALID_ECPOINTFORMAT_LIST);
- return 0;
- }
- }
-
- return 1;
-}
-
static int init_session_ticket(SSL_CONNECTION *s, unsigned int context)
{
if (!s->server)
return 1;
}
-int tls_parse_stoc_ec_pt_formats(SSL_CONNECTION *s, PACKET *pkt,
- unsigned int context,
- X509 *x, size_t chainidx)
-{
- size_t ecpointformats_len;
- PACKET ecptformatlist;
-
- if (!PACKET_as_length_prefixed_1(pkt, &ecptformatlist)) {
- SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
- return 0;
- }
- if (!s->hit) {
- ecpointformats_len = PACKET_remaining(&ecptformatlist);
- if (ecpointformats_len == 0) {
- SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_LENGTH);
- return 0;
- }
-
- s->ext.peer_ecpointformats_len = 0;
- OPENSSL_free(s->ext.peer_ecpointformats);
- s->ext.peer_ecpointformats = OPENSSL_malloc(ecpointformats_len);
- if (s->ext.peer_ecpointformats == NULL) {
- s->ext.peer_ecpointformats_len = 0;
- SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
- return 0;
- }
-
- s->ext.peer_ecpointformats_len = ecpointformats_len;
-
- if (!PACKET_copy_bytes(&ecptformatlist,
- s->ext.peer_ecpointformats,
- ecpointformats_len)) {
- SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
- return 0;
- }
- }
-
- return 1;
-}
-
int tls_parse_stoc_session_ticket(SSL_CONNECTION *s, PACKET *pkt,
unsigned int context,
X509 *x, size_t chainidx)
}
#endif
-int tls_parse_ctos_ec_pt_formats(SSL_CONNECTION *s, PACKET *pkt,
- unsigned int context,
- X509 *x, size_t chainidx)
-{
- PACKET ec_point_format_list;
-
- if (!PACKET_as_length_prefixed_1(pkt, &ec_point_format_list)
- || PACKET_remaining(&ec_point_format_list) == 0) {
- SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
- return 0;
- }
-
- if (!s->hit) {
- if (!PACKET_memdup(&ec_point_format_list,
- &s->ext.peer_ecpointformats,
- &s->ext.peer_ecpointformats_len)) {
- SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
- return 0;
- }
- }
-
- return 1;
-}
-
int tls_parse_ctos_session_ticket(SSL_CONNECTION *s, PACKET *pkt,
unsigned int context,
X509 *x, size_t chainidx)
{
unsigned long alg_k = s->s3.tmp.new_cipher->algorithm_mkey;
unsigned long alg_a = s->s3.tmp.new_cipher->algorithm_auth;
- int using_ecc = ((alg_k & SSL_kECDHE) || (alg_a & SSL_aECDSA))
- && (s->ext.peer_ecpointformats != NULL);
+ int using_ecc = (alg_k & SSL_kECDHE) || (alg_a & SSL_aECDSA);
const unsigned char *plist;
size_t plistlen;
- if (!using_ecc)
+ /*
+ * The extension is irrelevant unless we're negotiating an ECC
+ * ciphersuite at TLS 1.2 or below, and the peer sent a list. This
+ * is the first point at which the chosen ciphersuite is known, so
+ * the RFC 4492/8422 section 5.1.2 check for the required
+ * 'uncompressed' codepoint also happens here.
+ */
+ if (!using_ecc || s->ext.peer_ecpointformats == NULL)
return EXT_RETURN_NOT_SENT;
+ if (memchr(s->ext.peer_ecpointformats,
+ TLSEXT_ECPOINTFORMAT_uncompressed,
+ s->ext.peer_ecpointformats_len)
+ == NULL) {
+ SSLfatal(s, SSL_AD_ILLEGAL_PARAMETER,
+ SSL_R_TLS_INVALID_ECPOINTFORMAT_LIST);
+ return EXT_RETURN_FAIL;
+ }
+
tls1_get_formatlist(s, &plist, &plistlen);
if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_ec_point_formats)
|| !WPACKET_start_sub_packet_u16(pkt)
int tls_parse_ctos_early_data(SSL_CONNECTION *s, PACKET *pkt,
unsigned int context,
X509 *x, size_t chainidx);
-int tls_parse_ctos_ec_pt_formats(SSL_CONNECTION *s, PACKET *pkt,
- unsigned int context,
- X509 *x, size_t chainidx);
int tls_parse_ctos_supported_groups(SSL_CONNECTION *s, PACKET *pkt,
unsigned int context,
X509 *x, size_t chainidxl);
int tls_parse_stoc_maxfragmentlen(SSL_CONNECTION *s, PACKET *pkt,
unsigned int context,
X509 *x, size_t chainidx);
-int tls_parse_stoc_ec_pt_formats(SSL_CONNECTION *s, PACKET *pkt,
- unsigned int context,
- X509 *x, size_t chainidx);
int tls_parse_stoc_session_ticket(SSL_CONNECTION *s, PACKET *pkt,
unsigned int context,
X509 *x, size_t chainidx);
void tls1_get_formatlist(SSL_CONNECTION *s, const unsigned char **pformats,
size_t *num_formats)
{
- /*
- * If we have a custom point format list use it otherwise use default
- */
- if (s->ext.ecpointformats) {
- *pformats = s->ext.ecpointformats;
- *num_formats = s->ext.ecpointformats_len;
- } else if ((s->options & SSL_OP_LEGACY_EC_POINT_FORMATS) != 0) {
+ if ((s->options & SSL_OP_LEGACY_EC_POINT_FORMATS) != 0) {
*pformats = ecformats_all;
/* For Suite B we don't support char2 fields */
if (tls1_suiteb(s))
}
}
-/* Check a key is compatible with compression extension */
-static int tls1_check_pkey_comp(SSL_CONNECTION *s, EVP_PKEY *pkey)
-{
- unsigned char comp_id;
- size_t i;
- int point_conv;
-
- /* If not an EC key nothing to check */
- if (!EVP_PKEY_is_a(pkey, "EC"))
- return 1;
-
- /* Get required compression id */
- point_conv = EVP_PKEY_get_ec_point_conv_form(pkey);
- if (point_conv == 0)
- return 0;
- if (point_conv == POINT_CONVERSION_UNCOMPRESSED) {
- comp_id = TLSEXT_ECPOINTFORMAT_uncompressed;
- } else if (SSL_CONNECTION_IS_TLS13(s)) {
- /*
- * ec_point_formats extension is not used in TLSv1.3 so we ignore
- * this check.
- */
- return 1;
- } else {
- int field_type = EVP_PKEY_get_field_type(pkey);
-
- if (field_type == NID_X9_62_prime_field)
- comp_id = TLSEXT_ECPOINTFORMAT_ansiX962_compressed_prime;
- else if (field_type == NID_X9_62_characteristic_two_field)
- comp_id = TLSEXT_ECPOINTFORMAT_ansiX962_compressed_char2;
- else
- return 0;
- }
- /*
- * If point formats extension present check it, otherwise everything is
- * supported (see RFC4492).
- */
- if (s->ext.peer_ecpointformats == NULL)
- return 1;
-
- for (i = 0; i < s->ext.peer_ecpointformats_len; i++) {
- if (s->ext.peer_ecpointformats[i] == comp_id)
- return 1;
- }
- return 0;
-}
-
/* Return group id of a key */
static uint16_t tls1_get_group_id(EVP_PKEY *pkey)
{
/*
* Check cert parameters compatible with extensions: currently just checks EC
- * certificates have compatible curves and compression.
+ * certificates have compatible curves.
*/
static int tls1_check_cert_param(SSL_CONNECTION *s, X509 *x, int check_ee_md)
{
/* If not EC nothing to do */
if (!EVP_PKEY_is_a(pkey, "EC"))
return 1;
- /* Check compression */
- if (!tls1_check_pkey_comp(s, pkey))
- return 0;
group_id = tls1_get_group_id(pkey);
/*
* For a server we allow the certificate to not be in our list of supported
}
if (pkeyid == EVP_PKEY_EC) {
-
- /* Check point compression is permitted */
- if (!tls1_check_pkey_comp(s, pkey)) {
- SSLfatal(s, SSL_AD_ILLEGAL_PARAMETER,
- SSL_R_ILLEGAL_POINT_COMPRESSION);
- return 0;
- }
+ /*
+ * No point-format check on either the peer's or own cert.
+ * We accept any form we can decode, and send the cert we
+ * have.
+ */
/* For TLS 1.3 or Suite B check curve matches signature algorithm */
if (SSL_CONNECTION_IS_TLS13(s) || tls1_suiteb(s)) {
chain = cpk->chain;
strict_mode = c->cert_flags & SSL_CERT_FLAGS_CHECK_TLS_STRICT;
if (tls12_rpk_and_privkey(s, idx)) {
- if (EVP_PKEY_is_a(pk, "EC") && !tls1_check_pkey_comp(s, pk))
- return 0;
*pvalid = rv = CERT_PKEY_RPK;
return rv;
}
--- /dev/null
+-----BEGIN CERTIFICATE-----
+MIIBrzCCATSgAwIBAgIBAjAKBggqhkjOPQQDAjAbMRkwFwYDVQQDDBBFQ0RTQSBQ
+LTM4NCByb290MCAXDTI2MDYxOTE4MDA1OFoYDzIxMjYwNTI2MTgwMDU4WjAZMRcw
+FQYDVQQDDA5zZXJ2ZXIuZXhhbXBsZTA5MBMGByqGSM49AgEGCCqGSM49AwEHAyIA
+A4Mt9T6fKt3APp8/Frw65PDi2eMYdZK98nhBW9pA1Ccho4GIMIGFMB0GA1UdDgQW
+BBTozN8kakexZEnc26PUo5K2W9ptbTAfBgNVHSMEGDAWgBQm0I8de1/cHn9BgH1j
+yhx1gdaFaTAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
+AjAZBgNVHREEEjAQgg5zZXJ2ZXIuZXhhbXBsZTAKBggqhkjOPQQDAgNpADBmAjEA
+hvcNLTyL7vamQEJet5uvOXH7NKPHlG8sbfPvGS/AQ0yk6ARdc1Y1gV6FAnmnQUhX
+AjEA2HMTkh+h2ZDm68uRnhsNXIxI4dYF/nr5bdxw8XKN4P84Mg4gs37/IPoVN9jG
+Y4yh
+-----END CERTIFICATE-----
--- /dev/null
+-----BEGIN PRIVATE KEY-----
+MGcCAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcETTBLAgEBBCAb1VgxSUhJyh43soLb
+FMsebjWSp/Hma3kSyw6lT4txDaEkAyIAA4Mt9T6fKt3APp8/Frw65PDi2eMYdZK9
+8nhBW9pA1Cch
+-----END PRIVATE KEY-----
--- /dev/null
+#! /usr/bin/env perl
+# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+use strict;
+
+use OpenSSL::Test qw/:DEFAULT cmdstr srctop_file bldtop_dir/;
+use OpenSSL::Test::Utils;
+use TLSProxy::Proxy;
+use Cwd qw(abs_path);
+
+my $test_name = "test_ec_point_formats";
+setup($test_name);
+
+$ENV{OPENSSL_MODULES} = abs_path(bldtop_dir("test"));
+
+plan skip_all => "TLSProxy isn't usable on $^O"
+ if $^O =~ /^(VMS)$/;
+
+plan skip_all => "$test_name needs the module feature enabled"
+ if disabled("module");
+
+plan skip_all => "$test_name needs the sock feature enabled"
+ if disabled("sock");
+
+plan skip_all => "$test_name needs TLS 1.2 enabled"
+ if disabled("tls1_2");
+
+plan skip_all => "$test_name needs EC enabled"
+ if disabled("ec");
+
+# RFC 4492 section 5.1.2 requires the peer's ec_point_formats list to
+# contain "uncompressed", but only when an EC cipher suite is actually
+# negotiated at TLS 1.2 or below. The peer's list is irrelevant at TLS
+# 1.3 (the extension itself is) and at TLS 1.2 with a non-ECC cipher
+# suite. We mangle the list to contain only "compressed" (0x01) and
+# verify the four corner cases: both sides reject in an ECC TLS 1.2
+# handshake; both sides tolerate the missing "uncompressed" when TLS 1.3
+# or a non-ECC cipher suite is in play.
+
+# Wire format for ec_point_formats: 1-byte length prefix, then the
+# ECPointFormat bytes. "\x01\x01" advertises a list of one format,
+# the value 1 = ansiX962_compressed_prime; 0 (uncompressed) is absent.
+my $only_compressed = "\x01\x01";
+
+my $fatal_alert = 0;
+
+my $proxy = TLSProxy::Proxy->new(
+ \&mangle_ec_point_formats_clienthello,
+ cmdstr(app(["openssl"]), display => 1),
+ srctop_file("apps", "server.pem"),
+ (!$ENV{HARNESS_ACTIVE} || $ENV{HARNESS_VERBOSE}),
+);
+
+# Test 1: a non-conforming list in the ClientHello -- the server
+# must abort with illegal_parameter. ECDHE-RSA suffices to
+# trigger the check (client supports at least one ECDHE group and
+# cipher, so sends the extension), no EC cert required from either
+# side.
+$proxy->clientflags("-tls1_2");
+$proxy->serverflags("-tls1_2");
+$proxy->cipherc("ECDHE-RSA-AES128-SHA256");
+$proxy->ciphers("ECDHE-RSA-AES128-SHA256");
+$proxy->start() or plan skip_all => "Unable to start up proxy for tests";
+plan tests => 4;
+ok($fatal_alert,
+ "Server rejects ClientHello whose ec_point_formats omits uncompressed");
+
+# Test 2: a non-conforming list in the ServerHello -- the client must
+# abort with the same error.
+$fatal_alert = 0;
+$proxy->clear();
+$proxy->filter(\&mangle_ec_point_formats_serverhello);
+$proxy->clientflags("-tls1_2");
+$proxy->serverflags("-tls1_2");
+$proxy->cipherc("ECDHE-RSA-AES128-SHA256");
+$proxy->ciphers("ECDHE-RSA-AES128-SHA256");
+$proxy->start();
+ok($fatal_alert,
+ "Client rejects ServerHello whose ec_point_formats omits uncompressed");
+
+# Mutating a ClientHello on the wire breaks the handshake transcript
+# (the client and server compute different transcript hashes), so a
+# tolerance test can't wait for the handshake to complete -- the
+# Finished MAC will fail later regardless of whether the construct
+# hook rejected. Instead, the proxy is allowed to run to whatever
+# end it finds and we look at $proxy->message_list afterwards: if the
+# server sent a ServerHello in response to the mangled ClientHello,
+# it accepted the extension; if it rejected, the only server-side
+# message in the list will be the fatal alert.
+sub server_sent_hello {
+ my $proxy = shift;
+ foreach my $msg (@{$proxy->message_list}) {
+ return 1 if $msg->mt == TLSProxy::Message::MT_SERVER_HELLO;
+ }
+ return 0;
+}
+
+# Test 3: TLS 1.3 ignores the ec_point_formats extension entirely. The
+# server doesn't emit one in its TLS 1.3 ServerHello, and the
+# construct-hook 5.1.2 check requires an ECC TLS <= 1.2 ciphersuite
+# to fire. A mangled ClientHello list must therefore leave the server
+# willing to send its ServerHello.
+SKIP: {
+ skip "TLS 1.3 disabled", 1 if disabled("tls1_3");
+
+ $fatal_alert = 0;
+ $proxy->clear();
+ $proxy->filter(\&mangle_ec_point_formats_clienthello);
+ $proxy->clientflags("-tls1_3");
+ $proxy->serverflags("-tls1_3");
+ $proxy->start();
+ ok(server_sent_hello($proxy),
+ "TLS 1.3 server tolerates ec_point_formats missing uncompressed");
+}
+
+# Test 4: At TLS 1.2 with a non-ECC cipher suite, the construct hook
+# returns NOT_SENT without inspecting the peer's list. Offer ECDHE-RSA
+# alongside DHE-RSA on the client -- the ECDHE entry is enough for the
+# client to advertise ec_point_formats -- pin the server to DHE-RSA,
+# and confirm a mangled list is tolerated.
+SKIP: {
+ skip "DH disabled", 1 if disabled("dh");
+
+ $fatal_alert = 0;
+ $proxy->clear();
+ $proxy->filter(\&mangle_ec_point_formats_clienthello);
+ $proxy->clientflags("-tls1_2");
+ $proxy->serverflags("-tls1_2");
+ $proxy->cipherc("ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-GCM-SHA256");
+ $proxy->ciphers("DHE-RSA-AES128-GCM-SHA256");
+ $proxy->start();
+ ok(server_sent_hello($proxy),
+ "TLS 1.2 non-ECC server tolerates ec_point_formats missing uncompressed");
+}
+
+sub mangle_ec_point_formats_clienthello
+{
+ my $proxy = shift;
+
+ if ($proxy->flight == 0) {
+ foreach my $message (@{$proxy->message_list}) {
+ if ($message->mt == TLSProxy::Message::MT_CLIENT_HELLO) {
+ $message->set_extension(
+ TLSProxy::Message::EXT_EC_POINT_FORMATS,
+ $only_compressed);
+ $message->repack();
+ }
+ }
+ return;
+ }
+
+ my $last_record = @{$proxy->{record_list}}[-1];
+ $fatal_alert = 1
+ if defined $last_record && $last_record->is_fatal_alert(1);
+}
+
+sub mangle_ec_point_formats_serverhello
+{
+ my $proxy = shift;
+
+ if ($proxy->flight == 0) {
+ return;
+ } elsif ($proxy->flight == 1) {
+ foreach my $message (@{$proxy->message_list}) {
+ if ($message->mt == TLSProxy::Message::MT_SERVER_HELLO) {
+ $message->set_extension(
+ TLSProxy::Message::EXT_EC_POINT_FORMATS,
+ $only_compressed);
+ $message->repack();
+ }
+ }
+ return;
+ }
+
+ my $last_record = @{$proxy->{record_list}}[-1];
+ $fatal_alert = 1
+ if defined $last_record && $last_record->is_fatal_alert(0);
+}
@conf_srcs = glob(srctop_file("test", "ssl-tests", "*.cnf.in"));
# We hard-code the number of tests to double-check that the globbing above
# finds all files as expected.
- plan tests => 31;
+ plan tests => 32;
}
map { s/;.*// } @conf_srcs if $^O eq "VMS";
my @conf_files = map { basename($_, ".in") } @conf_srcs;
"28-seclevel.cnf" => disabled("tls1_2") || $no_ecx,
"30-extended-master-secret.cnf" => disabled("tls1_2"),
"32-compressed-certificate.cnf" => disabled("comp") || disabled("tls1_3"),
+ "33-compressed-spki.cnf" => disabled("tls1_2") || disabled("tls1_3") || $no_ec,
);
# Add your test here if it should be skipped for some compile-time
"26-tls13_client_auth.cnf" => disabled("tls1_3") || ($no_ec && $no_dh),
"29-dtls-sctp-label-bug.cnf" => disabled("sctp") || disabled("sock"),
"32-compressed-certificate.cnf" => disabled("comp") || disabled("tls1_3"),
+ "33-compressed-spki.cnf" => disabled("tls1_2") || disabled("tls1_3") || $no_ec,
);
foreach my $conf (@conf_files) {
--- /dev/null
+# Generated with generate_ssl_tests.pl
+
+num_tests = 2
+
+test-0 = 0-tls12-compressed-spki
+test-1 = 1-tls13-compressed-spki
+# ===========================================================
+
+[0-tls12-compressed-spki]
+ssl_conf = 0-tls12-compressed-spki-ssl
+
+[0-tls12-compressed-spki-ssl]
+server = 0-tls12-compressed-spki-server
+client = 0-tls12-compressed-spki-client
+
+[0-tls12-compressed-spki-server]
+Certificate = ${ENV::TEST_CERTS_DIR}/server-ec-compressed-cert.pem
+CipherString = DEFAULT
+ClientCAFile = ${ENV::TEST_CERTS_DIR}/p384-root.pem
+MaxProtocol = TLSv1.2
+MinProtocol = TLSv1.2
+PrivateKey = ${ENV::TEST_CERTS_DIR}/server-ec-compressed-key.pem
+VerifyCAFile = ${ENV::TEST_CERTS_DIR}/p384-root.pem
+VerifyMode = Require
+
+[0-tls12-compressed-spki-client]
+Certificate = ${ENV::TEST_CERTS_DIR}/server-ec-compressed-cert.pem
+CipherString = ECDHE-ECDSA-AES128-GCM-SHA256
+MaxProtocol = TLSv1.2
+MinProtocol = TLSv1.2
+PrivateKey = ${ENV::TEST_CERTS_DIR}/server-ec-compressed-key.pem
+VerifyCAFile = ${ENV::TEST_CERTS_DIR}/p384-root.pem
+VerifyMode = Peer
+
+[test-0]
+ExpectedClientCertType = P-256
+ExpectedProtocol = TLSv1.2
+ExpectedResult = Success
+ExpectedServerCertType = P-256
+
+
+# ===========================================================
+
+[1-tls13-compressed-spki]
+ssl_conf = 1-tls13-compressed-spki-ssl
+
+[1-tls13-compressed-spki-ssl]
+server = 1-tls13-compressed-spki-server
+client = 1-tls13-compressed-spki-client
+
+[1-tls13-compressed-spki-server]
+Certificate = ${ENV::TEST_CERTS_DIR}/server-ec-compressed-cert.pem
+CipherString = DEFAULT
+ClientCAFile = ${ENV::TEST_CERTS_DIR}/p384-root.pem
+MaxProtocol = TLSv1.3
+MinProtocol = TLSv1.3
+PrivateKey = ${ENV::TEST_CERTS_DIR}/server-ec-compressed-key.pem
+VerifyCAFile = ${ENV::TEST_CERTS_DIR}/p384-root.pem
+VerifyMode = Require
+
+[1-tls13-compressed-spki-client]
+Certificate = ${ENV::TEST_CERTS_DIR}/server-ec-compressed-cert.pem
+CipherString = DEFAULT
+MaxProtocol = TLSv1.3
+MinProtocol = TLSv1.3
+PrivateKey = ${ENV::TEST_CERTS_DIR}/server-ec-compressed-key.pem
+VerifyCAFile = ${ENV::TEST_CERTS_DIR}/p384-root.pem
+VerifyMode = Peer
+
+[test-1]
+ExpectedClientCertType = P-256
+ExpectedProtocol = TLSv1.3
+ExpectedResult = Success
+ExpectedServerCertType = P-256
+
+
--- /dev/null
+# -*- mode: perl; -*-
+# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+
+## End-to-end check that compressed-form EC leaf certificates
+## (server-ec-compressed-cert.pem, P-256 named curve, SPKI bit-string
+## leading byte 0x02 or 0x03, anchored to the P-384 EC root) work at
+## both TLS 1.2 and TLS 1.3 in both directions: the client presents
+## the same compressed leaf to a server that requires client
+## authentication, exercising X.509 acceptance of compressed point
+## form on both sides simultaneously. The ec_point_formats extension
+## no longer affects X.509 cert selection or acceptance, so neither
+## peer opts into LegacyECPointFormats; the default ec_point_formats
+## lists ('uncompressed' only) and the compressed leaves coexist.
+
+package ssltests;
+use OpenSSL::Test::Utils;
+
+our @tests = ();
+
+unless (disabled("ec") || disabled("tls1_2") || disabled("tls1_3")) {
+@tests = (
+ {
+ name => "tls12-compressed-spki",
+ server => {
+ "Certificate" => test_pem("server-ec-compressed-cert.pem"),
+ "PrivateKey" => test_pem("server-ec-compressed-key.pem"),
+ "VerifyCAFile" => test_pem("p384-root.pem"),
+ "ClientCAFile" => test_pem("p384-root.pem"),
+ "VerifyMode" => "Require",
+ "MinProtocol" => "TLSv1.2",
+ "MaxProtocol" => "TLSv1.2",
+ },
+ client => {
+ "Certificate" => test_pem("server-ec-compressed-cert.pem"),
+ "PrivateKey" => test_pem("server-ec-compressed-key.pem"),
+ "VerifyCAFile" => test_pem("p384-root.pem"),
+ "MinProtocol" => "TLSv1.2",
+ "MaxProtocol" => "TLSv1.2",
+ "CipherString" => "ECDHE-ECDSA-AES128-GCM-SHA256",
+ },
+ test => {
+ "ExpectedResult" => "Success",
+ "ExpectedProtocol" => "TLSv1.2",
+ "ExpectedServerCertType" => "P-256",
+ "ExpectedClientCertType" => "P-256",
+ },
+ },
+
+ {
+ name => "tls13-compressed-spki",
+ server => {
+ "Certificate" => test_pem("server-ec-compressed-cert.pem"),
+ "PrivateKey" => test_pem("server-ec-compressed-key.pem"),
+ "VerifyCAFile" => test_pem("p384-root.pem"),
+ "ClientCAFile" => test_pem("p384-root.pem"),
+ "VerifyMode" => "Require",
+ "MinProtocol" => "TLSv1.3",
+ "MaxProtocol" => "TLSv1.3",
+ },
+ client => {
+ "Certificate" => test_pem("server-ec-compressed-cert.pem"),
+ "PrivateKey" => test_pem("server-ec-compressed-key.pem"),
+ "VerifyCAFile" => test_pem("p384-root.pem"),
+ "MinProtocol" => "TLSv1.3",
+ "MaxProtocol" => "TLSv1.3",
+ },
+ test => {
+ "ExpectedResult" => "Success",
+ "ExpectedProtocol" => "TLSv1.3",
+ "ExpectedServerCertType" => "P-256",
+ "ExpectedClientCertType" => "P-256",
+ },
+ },
+);
+}
{
SSL_CTX *cctx = NULL, *sctx = NULL;
SSL *clientssl = NULL, *serverssl = NULL;
- const char *pformats = NULL;
+ const unsigned char *pformats = NULL;
int nformats;
int testresult = 0;
SSL_CTX_set1_client_certificate_types(3)
SSL_set1_client_certificate_types(3)
SSL_get0_raw_cipherlist(3)
-SSL_get0_ec_point_formats(3)
SSL_CTX_need_tmp_RSA(3)
SSL_CTX_set_tmp_rsa(3)
SSL_need_tmp_RSA(3)
SSL_disable_ct define
SSL_get0_chain_certs define
SSL_get0_iana_groups define
+SSL_get0_ec_point_formats define
SSL_get0_session define
SSL_get0_chain_cert_store define
SSL_get0_verify_cert_store define